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,还是别的支持的协议都用那一套,而不同的协议之间是有很大差别的,而这个结构体实现了统一的接口。

我们看看它的实现代码:

来源:/usr/include/bits/socket.h

结构体有两个内容:

  • __SOCKADDR_COMMON:注释是地址族,也就是上面提到的 "AF__xxx"
  • sa_data:表示地址信息,共14个字节,既然是地址信息,那就有端口(2个字节),IP地址(v4占用4个字节),如果要用IPv6,这个长度还不够,因为IPv6是128位16个字节。

接着往下看__SOCKADDR_COMMON:

__SOCKADDR_COMMON

来源:/usr/include/bits/sockaddr.h

这里有两个定义,第一个定义是将sa_prefix与family连起来,比如:sa_prefix表示AF,则是AF_family。第二个定义是无符号短整型,表示数据类型。

关键点在于最上面那一段注释:此宏用于声明初始公共成员用于socket地址的数据类型,这里提到了三个:

struct sockaddr,struct sockaddr_in,struct sockaddr_un

第一个前面以及提到了,接下来看后面这两个。

sockaddr_in

直接看源码:

来源:/usr/include/netinet/in.h

这个结构体的第一个成员上面说了,主要的是这两个标绿的成员,通过注释咱们也能看到这表示的是端口和地址。最后一个成员是待填充的8个字节(sockaddr的大小减去地址族类型、减去端口和IP),可以理解为保留空间。端口和地址的具体定义也在同一个文件中,可以看到,端口是16位,地址是32位的无符号整形数,也就是只能存IPv4。

  • in_por_t
  • in_addr

sockaddr_un

直接看定义:

来源:/usr/include/sys/un.h

当地址族是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源码。

编辑于 2022-05-27 11:22