Linux TCP 连接基本套接字函数
2023-06-10

主要参考自人民邮电出版社 《Unix 网络编程 卷一:套接字联网 API》

TCP 通信流程与基本的套接字函数

下图描述了 TCP 客户端/服务端程序涉及到的基本的套接字函数及其调用流程:

TCP 系统调用.jpg

下面把上图中的函数分成连接建立、数据传输和连接关闭三类进行详细分析。

建立连接

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,则使用 familytype 组合的默认协议,也可以指定特定协议,对于 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);

其中:

  • sockfdlisten() 监听的套接字描述符;
  • cliaddr 是一个特定地址结构体,用来存储返回的客户端地址信息;
  • addrlen 是地址结构体长度,调用时传入 cliaddr 结构体的长度,返回时被设置为返回的结构体的确切长度;
  • 该函数成功时,返回一个套接字描述符,一般称之为已连接套接字描述符,连接成功建立后,数据传输都是对于该已连接套接字描述符的操作。

关闭连接

close()

close() 函数的定义为:

#include <unistd.h>
int close(int sockfd);

close 一个 TCP 连接套接字的默认行为是把该套接字标记成已关闭,然后立即返回到调用进程。该套接字描述符不能再由调用进程使用,也就是不能再作为 readwrite 的第一个参数。然而 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 各一次。