主要参考自人民邮电出版社 《Unix 网络编程 卷一:套接字联网 API》
TCP 通信流程与基本的套接字函数
下图描述了 TCP 客户端/服务端程序涉及到的基本的套接字函数及其调用流程:
下面把上图中的函数分成连接建立、数据传输和连接关闭三类进行详细分析。
建立连接
socket()
socket()
函数的头文件和函数定义为:
#include <sys/socket.h>
int socket(int family, int type, int protocol);
// 成功则返回一个非负值,代表一个套接字描述符,类似于文件描述符,失败则返回 -1。
其中:
-
family
中常用的有AF_INET
,AF_INET6
,AF_LOCAL
等,分别代表 IPv4 协议,IPv6 协议,Unix 域协议等。 -
type
中常用的有SOCK_STRAM
,SOCK_DGRAM
,SOCK_RAW
等,分别代表 字节流套接字,数据报套接字,原始套接字。 -
protocol
如果置为 0,则使用family
和type
组合的默认协议,也可以指定特定协议,对于 TCP 和 UDP 而言,这个参数可以置为 0.
bind()
bind()
函数的头文件和函数定义为:
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
其中:
-
sockfd
是一个套接字描述符,也就是socket()
函数的返回值; -
sockaddr
是一个指向特定协议的地址结构的指针,对于 TCP/UDP 而言,这里可以用sockaddr_in
结构体,其不精确定义(实际定义中要较多的宏定义)如下:#include<netinet/in.h> struct in_addr { in_addr_t s_addr; } struct sockaddr_in { sa_family_t sin_family; in_port_t sin_port; struct in_addr sin_addr; char[] sin_zero; }
可以看到,
struct in_addr
是一个包含 IP 地址的结构体,然后sockaddr_in
中有sa_family
描述的协议族,sin_port
描述的端口号,sin_addr
描述的 IP 地址,以及一个额外的sin_zero
字段。 -
addrlen
是地址结构体的长度。
端口被占用的错误一般出现在这一步。
listen()
listen()
函数的定义为:
#include<sys/socket.h>
int listen(int sockfd, int backlog);
其中:
sockfd
是套接字描述符,一般在服务端,这里的sockfd
应该是调用bind()
绑定了 IP 和端口的套接字描述符;backlog
可以粗略理解为一个连接队列正相关的数值,用于描述连接处理队列的长度,具体含义我会再写一篇博客。目前可以有一点大概理解,过大会导致服务激增师队列本身占用大量资源,影响网络 IO, 过小会导致不能充分发挥服务器的性能。
connect()
connect()
函数的定义为:
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
其中:
sockfd
是一个套接字描述符,也就是socket()
函数的返回值,需要注意的是,对于主动发起连接的客户端,调用connect()
主动发起连接之前,调用bind()
不是必须的,如果不调用,内核可以自动分配一个 IP 和端口;sockaddr
如同之前的分析,对于 TCP 连接,可以用sockaddr_in
结构体,这里用于指定服务端的 IP 和端口;addrlen
是地址结构体的长度。
accept()
accept()
函数的定义为:
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
其中:
sockfd
是listen()
监听的套接字描述符;cliaddr
是一个特定地址结构体,用来存储返回的客户端地址信息;addrlen
是地址结构体长度,调用时传入 cliaddr 结构体的长度,返回时被设置为返回的结构体的确切长度;- 该函数成功时,返回一个套接字描述符,一般称之为已连接套接字描述符,连接成功建立后,数据传输都是对于该已连接套接字描述符的操作。
关闭连接
close()
close()
函数的定义为:
#include <unistd.h>
int close(int sockfd);
close
一个 TCP 连接套接字的默认行为是把该套接字标记成已关闭,然后立即返回到调用进程。该套接字描述符不能再由调用进程使用,也就是不能再作为 read
和 write
的第一个参数。然而 TCP 将尝试发送以排队的数据,发送完毕后进入正常的终止序列。
在多进程情景下,close
并不会直接关闭套接字,而是把套接字描述符的引用计数减 1,这刚好满足多进程情景下的期望。
shutdown()
shutdown()
函数的定义为:
#include <sys/socket.h>
int shutdown(int sockfd, int howto);
这个函数的行为取决于 howto
的值:
- SHUT_RD 关闭连接的读一半,套接字中不再有数据可以接受,不可以对这个套接字再执行任何读函数,所有收到的数据会被确认后丢弃。
- SHUT_WR 关闭连接的写一半,TCP 中对应半关闭,套接字缓冲区中的数据会被发送,后跟正常终止序列。此时不论套接字引用计数是否等于0,均会关闭写一半。进程不能再对这样的套接字执行任何写函数。
- SHUT_RDWR 关闭读和写,相当于分别调用 SHUT_RD 和 SHUT_WR 各一次。