socket(TCP)通信的相关函数
概览
使用socket进行TCP的通信过程画个图表示一下:
这个过程是比较直观的,客户端和服务端要进行通信,我们简要分析一下:
- 客户端想要与服务端进行通信,则客户端自己要请求建立连接,只有建立了连接,双方才能收发数据,就好比打视频电话,我打过去,对方接了才能说话。
- 服务端要准备好随时处理来自外部客户端的通信请求。打视频电话时,如果对方根本没有登录微信/QQ,那也是打不通的,只有对方上线了,打过去的微信电话才能被对方收到。
- 建立连接的过程就是三次握手。
- TCP通信涉及到一个四元组:源IP、目的IP、源端口、目的端口。IP地址是表示数据包从哪个主机来到哪个主机去,端口是表示数据从客户端的哪个进程传出来,到服务端的哪个进程去。
- 客户端结束连接,这是四次挥手。
逐个分析
这里通过查看Linux下的man命令来分析和学习这些函数。
socket
这个函数的作用是初始化一个socket,相关的参数和描述如下:
意思是创建一个通信的端点,并且返回一个文件描述符。参数有三个:
- domain :协议族,表示用哪种协议进行通信,这些协议族在<sys/socket.h>文件中,如下
一般用到的就是本地进程间通信,可以使用AF_UNIX,AF_LOCAL;IPv4用AF_INET,IPv6用AF_INET6。
- type :指的是通信语义,目前有这几类:
常用的也就是sock_stream,流式传输,默认是TCP协议;sock_dgram,数据报形式,默认是UDP协议。
- protocol :指定要与socket一起使用的特定协议,一般可以置为0,表示使用默认的,比如sock_stream默认是TCP协议。
- 返回值
成功则返回一个用于socket通信的文件描述符,否则返回-1。
bind
将socket的文件描述符绑定端口和IP地址,具体的描述如下:
在accept之前,必须要先绑定。它的参数如下:
- sockfd
通过socket函数得到的文件描述符。
- addr
这是个结构体,类型是sockaddr,这个结构体的具体含义,它存的是IP和端口的网络字节序。这个结构体的详细内容留在最后去说。
- addrlen
表示上述结构体的内存大小。
- 返回值
成功则返回0,出错则返回-1。
listen
监听是否有客户端连接请求,它传入上面bind的socket,另一个参数是请求连接队列的最大长度,这个队列指的是多个客户端都请求建立连接时,会把这些连接暂时存入队列中,以便下一步调用accept接受连接。
成功则返回0,否则-1。
accept
这个函数等待并接受客户端的连接请求, 建立新的连接, 会得到一个新的(用于通信的)文件描述符。
它有两种形式,一般都只用accept,可以看一下两者的区别:
其余的参数上面已经介绍过了,只不过这里的文件描述符sockfd是经过bind、listen的那个文件描述符。
此外,这里的 addr 参数是传出的,函数成功执行后,存的是客户端的IP和端口。
这个函数的重要性质是:阻塞。在调用它时,会从监听的连接队列里取出一个请求,然后创建一个新的连接好的socket,并返回指向这个socket的文件描述符,而原始的socket不受影响。如果这个队列为空,函数将阻塞直到队列不为空。
- 返回值
如果成功,则会返回一个非负整数,也就是accepted socket的文件描述符,后续的通信则用这个文件描述符,如果失败则返回-1。
connect
用于向目的IP和目的端口请求建立连接。
参数都介绍过了,第一个是文件描述符,是由socket函数创建的,第二个保存的是服务端的IP和端口,也是大端的格式,第三个是addr的内存大小。
- 返回值
0表示成功,-1表示错误失败。
发送数据(send/write)
(1)send
相关的函数有三个,如下:
用途都是把数据从本地发送出去,sockfd表示要发送socket的文件描述符,buf表示要发送的的数据,len为发送数据的长度,flags为可选参数,一般为0。
一般用send函数即可。
返回值:如果成功发送,则返回发送的字节数,应该等于len,否则返回-1。
(2)write
这个是向一个文件描述符写入buf的数据,write等同于上面的send函数在flags为0。
写入成功时会返回字节数,否则返回-1。
接收数据(recv/read)
(1)recv
这个函数与send函数对应,一个发送,一个接收。
参数都介绍过了,这里是把数据写向buf,返回值则是接受的字节数或者-1,如果连接中断则返回0。
这个函数的重要性质是阻塞:连接建立的情况下,若没有数据收到,则会阻塞,直到有数据收到。
返回的数据量是当前收到的,而不会等到所有都收到才返回。
(2)read
这个与write对应,就不多说了:
总结一下:
- send/write:把buf的数据传出去
- recv/read:把接收到的数据存到buf
close
关闭文件描述符,成功则返回0,否则返回-1。
在通信结束时,要把所有的文件描述符都关闭掉。
sockaddr是什么?
如果嫌文字太麻烦,可以直接跳转到下面的 “小的总结”看图解。
上面的一些函数中,经常看到这样的一个参数:
const struct sockaddr *addr
首先,这是一个结构体,它的作用是存放IP地址和端口的,要知道,上面提到的那些函数都是通用的,不管是IPv4还是IPv6,还是别的支持的协议都用那一套,而不同的协议之间是有很大差别的,而这个结构体实现了统一的接口。
我们看看它的实现代码:
结构体有两个内容:
- __SOCKADDR_COMMON:注释是地址族,也就是上面提到的 "AF__xxx"
- sa_data:表示地址信息,共14个字节,既然是地址信息,那就有端口(2个字节),IP地址(v4占用4个字节),如果要用IPv6,这个长度还不够,因为IPv6是128位16个字节。
接着往下看__SOCKADDR_COMMON:
__SOCKADDR_COMMON
这里有两个定义,第一个定义是将sa_prefix与family连起来,比如:sa_prefix表示AF,则是AF_family。第二个定义是无符号短整型,表示数据类型。
关键点在于最上面那一段注释:此宏用于声明初始公共成员用于socket地址的数据类型,这里提到了三个:
struct sockaddr,struct sockaddr_in,struct sockaddr_un
第一个前面以及提到了,接下来看后面这两个。
sockaddr_in
直接看源码:
这个结构体的第一个成员上面说了,主要的是这两个标绿的成员,通过注释咱们也能看到这表示的是端口和地址。最后一个成员是待填充的8个字节(sockaddr的大小减去地址族类型、减去端口和IP),可以理解为保留空间。端口和地址的具体定义也在同一个文件中,可以看到,端口是16位,地址是32位的无符号整形数,也就是只能存IPv4。
- in_por_t
- in_addr
sockaddr_un
直接看定义:
当地址族是AF_LOCAL时,是不是指本地进程间通信呢?对吧。
sockaddr_in6
在查看sockaddr_in时,会发现下面紧挨着一个sockaddr_in6的结构体定义:
可以看到:端口信息16位,然后是32位的流信息,然后是一个in6_addr结构体表示IPv6地址,然后是一个32位的scope-id。
这个in6_addr表示为:
这里面又包含了三个结构体,主要看第一个:
uint8_t __u6_addr8[16];
意思是一个长度为16的数组,每个单元存放8位,总共就是128位,恰好存放IPv6地址。
另外两个数组也是表示128位的。
小的总结
上面提到了4个结构体:sockaddr,sockaddr_in,sockaddr_un,sockaddr_in6,接下来我用图解描述一下:
文件描述符
上面也多次提到了文件描述符这个概念,这里简单说明一下:
文件描述符(file descriptor)是内核为了高效管理已被打开的文件所创建的索引,用于指代被打开的文件,对文件所有 I/O 操作相关的系统调用都需要通过文件描述符。
相关的文章详细讲解了文件描述符:
说明
本文对socket通信所使用到的一些基本的函数进行了粗略的介绍,相关的函数详细说明可以查看man文档,或者直接查看Linux源码。