- TCP通信的代码举例
- 实现TCP服务器程序
- 第1步:调用socket 网络API,创建套接字文件
- socket函数
- 函数原型
- 功能
- 返回值
- 参数
- domian 指定协议族
- type 套接字类型
- protocol 指定协议号
- 代码演示
TCP通信的代码举例
· 写一个TCP服务器程序
· 写一个客户端程序
然后客户端向服务器请求连接,当连接成功之后,服务器和客户端程序实现通信。
不过为了不要让例子太复杂,服务器程序目前只为一个客户服务,至于使用多进程、多线程以及多路io为多客户服务的情况,后面进行说明。
实现TCP服务器程序
按照TCP编程模型来编写
第1步:调用socket 网络API,创建套接字文件
socket函数
函数原型
1 2 3 | #include <sys/types.h> #include <sys/socket.h> int socket(int domain, int type, int protocol); |
功能
创建一个套接字文件,然后以文件形式来操作通信,不过套接字文件没有文件名。
Linux有7种文件,套接字文件就是其中一种。
socket翻译为中文就是“套接字”的意思,其实翻译为插座更合适些,因为socket本来就有插座的意思,使用socket创建一个通信的套接字文件,就好比给应用程序插上了一个通信的插座,有了这个插座就可以和对方应用程序通信了。
图解说明:
返回值
- 成功:返回套接字文件描述符。
- 失败:返回-1,errno被设置。
参数
domian 指定协议族
族/范围
1 | int socket(int domain, int type, int protocol); |
- 作用:指定协议族
为什么要指定协议族?
因为你要使用的通信协议一定是属于某个协议族,所以如果不指定协议族,又怎么指定协议族中的某个具体协议呢。
比如我们想用的是TCP协议,TCP属于TCP/IP协议族中的子协议,所以必须先通过domain指定TCP/IP协议族,不过TCP/IP协议族有两个版本,分别是IPV4是IPV6版本,我们目前使用的还是IPV4版本,因为Ipv6还未普及。
IPV4是Internet Protocol Version4的缩写,直译为“网络协议第四版”,IPV4和IPV6这两个版本所使用的ip地址格式完全不同,
IPV4:ip为32位
IPV6:ip为128位
不仅IPV4和IPV6的ip地址格式不同,其实所有不同网络协议族所使用ip地址格式都不相同。
1 | int socket(int domain, int type, int protocol); |
- domain可设置的常见宏值。
可设置的有:AF_UNIX, AF_LOCAL(前面两个宏明表示的意义是一样的)、AF_INET、AF_INET6AF_IPXAF_NETLINKAF_APPLETALKAF_PACKET、AF_UNSPEC,有关每个宏的作用后面再解释,这里先介绍下这些“宏名”。
AF是address family,表示地址家族的意思,由于每个网络协议族的ip地址格式完全不同,因此在指定ip地址格式时需要做区分,所以这些AF_***宏就是用于说明所使用的是什么协议族的IP地址,也就是说定义这些宏的目的是用来区分不同协议族的IP地址格式。
这些个宏定义在了socket.h中
我们下面给出一部分宏名对应的内容。
1 2 3 4 5 6 | #define AF_UNSPEC 0 #define AF_UNIX 1 /* Unix domain sockets */ #define AF_LOCAL 1 /* POSIX name for AF_UNIX */ #define AF_INET 2 /* Internet IP Protocol */ #define AF_AX25 3 /* Amateur Radio AX.25 */ ... |
有读者可能会说不对呀
domain不是用来指定协议族的吗,但是AF_ * * * 确是用来区分不同协议ip格式的,给domain指定AF_ * * * 合适吗?
其实是合适的,没有问题。实际上区分不同协议族应该使用PF_UNIX, PF_LOCAL、PF_INET、PF_INET6、PF_IPX、PF_NETLINK、PF_APPLETALK、PF_PACKET、PF_UNSPEC,PF就是protocol family的意思,意思是“协议家族”。
PF_***定义的宏与AF_***定义的宏不同的只是前缀,不过AF_***宏与PF_***宏对应的值完全一样,比如AF_UNIX == PF_UNIX,所以给domain指定AF_***,与指定PF_***完全是一样的。
为什么AF_ * * * 定义的宏 == PF_ * * * 定义的宏?
AF_***用于区分不同协议族的ip地址格式,而PF_***则用于区分不同的协议族,但是每个协议族的IP格式就一种,所以协议族与自己的IP格式其实是一对一的,因此如果你知道使用的是什么ip地址格式,其实你也就知道了使用的是什么协议族,所以使用AF_***定义的宏也可以用于区分不同的协议族。
不过为了更加正规一点,区分不同协议族的宏还是被命名为了PF_***,只不过它的值就是AF_***的值。
1 2 3 4 5 6 | #define PF_UNSPEC AF_UNSPEC #define PF_UNIX AF_UNIX #define PF_LOCAL AF_LOCAL #define PF_INET AF_INET #define PF_AX25 AF_AX25 ... |
1 | int socket(int domain, int type, int protocol); |
总之希望读者理解,domain是用于指定协议族的,设置的宏可以是AF_***,也可以是PF_***,不过正规一点的话还是应该写PF_***,因为这个宏才是专门用来区分协议族的,而AF_***则是用来区分不同协议族的ip地址格式的。
不过socket的手册里面写的都是AF_***,没有写PF_***。
- domain的常见宏值,各自代表是什么协议族,制定不同的宏就代表不同的协议族。
AF_UNIX, AF_LOCAL、AF_INET、AF_INET6、AF_IPX、AF_NETLINK、AF_APPLETALK、AF_PACKET、AF_UNSPEC。
上面宏值与下面的宏值对应相等。
PF_UNIX, PF_LOCAL、PF_INET、PF_INET6、PF_IPX、PF_NETLINK、PF_APPLETALK、PF_PACKET、PF_UNSPEC。
- PF_UNIX、PF_LOCAL:域通信协议族
这两个宏值是一样(宏值都是1)。
给domain指定该宏时就表示,要使用的是“本机进程间通信”协议族,域通信也就是本机进程间通信,我们在说明IPC时就说过,还有一种域套接字的IPC,也可以专门用来实现“本机进程间通信”,域套接字IPC就是我们这里说明的域通信。
这里域就是本机的意思,当我们给socket的domain指定这两个宏时,创建的就是域套接字文件,这个套接字文件是用来实现域通信的,所以把创建的文件就称为域套接字文件。
后面说明域套接字通信时,会具体的来使用这个宏。
-
PF_INET:指定ipv4的TCP/IP协议族。
我们这篇博客要说明的TCP和UDP就是IPV4的TCP和UDP。 -
PF_INET6:ipv6的TCP/IP协议族,目前还未普及使用,我们这里不说明。
-
PF_IPX:novell协议族,几乎用不到,了解即可。
novell协议族由美国Novell网络公司开发的一种专门针对局域网的“局域网通信协议”。
这个协议的特点是效率较高,所以好多局域网游戏很喜欢使用这个协议来进行局域网通信,比如以前的局域网游戏CS,据说使用的就是novell协议族。
之所以使用novell协议族,是因为CS的画面数据量太大,而且协同性要求很高,所以就选择了使用novell协议族这个高效率的局域网协议。
现在互联网使用的都是TCP/IP协议,而novell和TCP/IP是两个完全不同的协议,所以使用novell协议族的局域网与使用TCP/IP协议族的互联网之间兼容性比较差,如果novell协议的局域网要接入TCP/IP的Internet的话,数据必须进行协议转换。
所谓协议转换就是
novell局域网的数据包发向TCP/IP的互联网时,将novell协议族的数据包拆包,然后重新封包为TCP/IP协议的数据包。
TCP/IP的互联网数据包发向novell局域网时,将TCP/IP协议族的数据包拆包,然后重新封包为novell协议的数据包。
windows似乎并不是支持novell协议,但是Linux、unix这边是支持的。
我怎么知道是支持的?
既然我们在socket手册中查到了PF_IPX,就说明时肯定支持。
-
PF_APPLETALK:苹果公司专为自己“苹果电脑”设计的局域网协议族。
-
AF_UNSPEC:不指定具体协议族
int socket(int domain, int type, int protocol);
此时可以通过第三个参数protocol指定协议编号来告诉socket,你使用的是什么协议族中的哪个子协议。
什么是协议编号?
每个协议的唯一识别号,制订协议的人给的。
type 套接字类型
套接字类型,说白了就是进一步指定,你想要使用协议族中的那个子协议来通信。
比如,如果你想使用TCP协议来通信,
首先:将domain指定为PF_INET,表示使用的是IPV4的TCP/IP协议族
其次:对type进行相应的设置,进一步表示我想使用的是TCP/IP协议族中的TCP协议。
type的常见设置值:SOCK_STREAM、SOCK_DGRAM、SOCK_RDM、SOCK_NONBLOCK、SOCK_CLOEXEC。
- SOCK_STREAM:
1 | int socket(int domain, int type, int protocol); |
将type指定为SOCK_STREAM时,表示想使用的是“有序的、面向连接的、双向通信的、可靠的字节流通信”,并且支持带外数据。
有关什么是带外数据我们后面进行说明。
stream 就是字节流的意思。
1 | int socket(int domain, int type, int protocol); |
如果domain被设置为PF_INET,type被设置为SOCK_STREAM,就表示你想使用TCP来通信,因为在TCP/IP协议族里面,只有TCP协议是“有序的、面向连接的、双向的、可靠的字节流通信”。
使用TCP协议通信时TCP并不是孤立的,它还需要网络层和链路层协议的支持才能工作。
如果type设置为SOCK_STREAM,但是domain指定为了其它协议族,那就表示使用的是其它“协议族”中类似TCP这样的可靠传输协议。
- SOCK_DGRAM:
1 | int socket(int domain, int type, int protocol); |
将type指定为SOCK_DGRAM时,表示想使用的是“无连接、不可靠的、固定长度的数据报通信”。
固定长度意思是说,分组数据的大小是固定的,不管网络好不好,不会自动去调整分组数据的大小,所以“固定长度数据报”其实就是“固定长度分组数据”的意思。
当domain指定为PF_INET、type指定为SOCK_DGRAM时,就表示想使用的是TCP/IP协议族中的中的UDP协议,因为在TCP/IP协议族中,只有UDP是“无连接、不可靠的、固定长度的数据报通信”。
同样的UDP协议通信不可能独立工作,需要网络层和链路层协议的支持。
如果type设置为SOCK_DGRAM,但是domain指定为了其它协议族,那就表示使用的是其它“协议族”中类似UDP这样的不可靠传输协议。
后面说明UDP时就会使用SOCK_DGRAM。
- SOCK_RDM:表示想使用的是“原始网络通信”。
1 | int socket(int domain, int type, int protocol); |
当domain指定为TCP/IP协议族、type指定为SOCK_RDM时,就表示想使用ip协议来通信,直接使用IP协议来通信,其实就是原始的网络通信。
我们在前面说明TCP/IP协议族的时候,应用程序可以直接越过TCP协议或者UDP协议,直接使用IP协议进行通信,也就是原始的网络通信。
为什么称为原始通信?
以TCP/IP协议族为例,TCP/IP之所以能够实现网络通信,最根本的原因是因为IP协议的存在,ip协议才是关键,只是原始的IP协议只能实现最基本的通信,还缺乏很多精细的功能,所以才多了基于IP工作的TCP协议和UDP协议这两种传输层协议,TCP/UDP弥补了IP缺失的精细功能。
尽管ip提供的只是非常原始的通信,但是我们确实可以使用比较原始的IP协议来进通信,特别是当你不满意TCP和UDP的表现,你想实现一个比TCP和UDP更好的传输层协议时,你就可以直接使用ip协议,然后由自己的应用程序来实现符合自己要求的类似tcp/udp协议,不过我们几乎遇不到这种情况,这里了解下即可。
如果type设置为SOCK_RDM,但是domain指定为了其它协议族,那就表示使用的是其它“协议族”中类似ip这样最基本的原始通信协议。
- SOCK_NONBLOCK:将socket返回的文件描述符指定为非阻塞的。
1 | int socket(int domain, int type, int protocol); |
如果不指定这个宏的话,使用socket返回“套接字文件描述符”时,不管是是用来“监听”还是用来通信,都是阻塞操作的,但是指定SOCK_NONBLOCK这个宏的话,就是非阻塞的。
当然也可以使用fcntl函数来指定SOCK_NONBLOCK,至于fcntl怎么用,我们在高级IO博客中已经有非常详细的说明。
SOCK_NONBLOCK宏可以和前面的宏进行 | 运算,比如:
SOCK_STREAM | SOCK_NONBLOCK
上面的宏表示使用面向连接的、可靠的字节流通信,同时socket返回的文件描述符非阻塞。
- SOCK_CLOEXEC:表示一旦进程exec执行新程序后,自动关闭socket返回的“套接字文件描述符”。
1 | int socket(int domain, int type, int protocol); |
图解说明:
再次说明,type指定 SOCK_CLOEXEC 宏,一旦进程通过调用exec函数去执行新程序,之前调用socket函数返回的套接字文件描述符将会自动关闭,在新程序里面无法使用上面的套接字文件描述符sockfd。
这个标志也是可以和前面的宏进行 | 运算的,不过一般不指定这个标志。
例如:SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC
1 | int socket(int domain, int type, int protocol); |
protocol 指定协议号
一般情况下protocol写0,表示使用domain和type所指定的协议。
不过如果domain和type所对应的协议有好几个时,此时就需要通过具体的协议号来区分了,否则写0即可,表示domain和type所对应的协议就一个,不需要指定协议号来区分。
一般情况下使用TCP的时候,domain和type 两个参数所指定的TCP协议就只有一个,
所以第三个参数设置为0即可。
在哪里可以查到协议号?
所有协议的协议号都被保存在了/etc/protocols下。
打开文件查看协议号:
协议 编号
ip 对应 0
icmp 对应 1
igmp 对应 2
tcp 对应 6
udp 对应 17
等
如果现在要调用socket 函数使用tcp协议,那么通过下面方式指定参数:
1 | socket(PF_INET, SOCK_STREAM, 0); |
我们可以看到上面指定的TCP协议标号为6。
如果调用socket 函数为:
1 | socket(PF_INET, SOCK_STREAM, 6); |
也不会出现错误,相当于重复指定。通过前两个参数本来就可以指定TCP协议,结果又指定了TCP协议号。不过一般来说如果前面两个参数能够确定协议,那么就不指定协议编号,协议编号写0即可。因为指定协议号会更加麻烦,而且协议号也记不住。除非前面两个参数所对应的协议分成了好几个子协议,那么就必须通过协议编号进行区分。
代码演示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <string.h> #include <strings.h> #include <errno.h> #include <sys/stat.h> void print_err(char * str,int line,int err_no)//出错处理函数 { printf("%d,%s: %s\n",line,str,strerror(err_no)); exit(-1); } int main(void) { int sockfd = -1; //存放套接字文件描述符 /*创建使用TCP协议通信的套接字文件*/ sockfd = socket(PF_INET,SOCK_STREAM,0); //指定TCP协议 if(-1 == sockfd) //进行出错处理 print_err("socket fail",__LINE__,errno); return 0; } |
运行结果为:
代码运行没有问题。下篇博客进行第二步:调用bind函数。