IMPORTANT

此文档仍需调整补充。

TCP 传输控制协议

Transmission Control Protocol,是一种有连接的,可靠的,以字节流形式进行传输的传输层协议。

常规通信过程

服务器 Server客户端 Client
socket() 创建套接字socket() 创建套接字
bind() 绑定地址(常由操作系统隐式完成)
listen() 监听端口/
accpet() 接受连接connect() 请求连接服务器
send() / recv() 数据传输send() / recv() 数据传输
close() 关闭连接close() 关闭连接

Info

服务器接受新连接后,必须通过与此客户端连接的新套接字与客户端通信。

函数及用法

Info

函数形式来自 Ubuntu 22.04,仅适用于 Linux / Unix。

使用以下函数,需要导入 sys/socket.h

创建套接字

int socket(int __domain, int __type, int __protocol)

参数

  1. 传输要使用的协议族,可用的协议族有: AF_INET IPv4 协议; AF_INET6 IPv6 协议; AF_UNIX / AF_LOCAL 本机通信;

    AF_PACKET 数据链路层原始帧通信;

    AF_NETLINK 内核与用户空间通信。

  2. 套接字的数据传输方式,可用的传输方式有: SOCK_STREAM 有连接的、流式数据传输(TCP 使用); SOCK_DGRAM 无连接的数据报传输(UDP 使用); [需要 root] SOCK_RAW 不使用传输层协议,直接到达网络层; SOCK_SEQPACKET 有序分组套接字; 同时允许与以下选项共用: SOCK_NONBLOCK 停用等待; SOCK_CLOEXEC 运行 exec 系列函数时,关闭套接字。

  3. 协议,设为0时自动选择,可用的协议有: IPPROTO_ICMP 使用 ICMP 协议; IPPROTO_RAW 手动构建 IP 数据报头部。

返回值

创建成功时,返回套接字文件描述符,其他情况返回-1并设置 errno 值;

错误的协议与传输方式组合,非 root 状态使用原始帧通信,都将返回-1。

将套接字绑定至地址与端口

Info

Q. 为什么服务器需要绑定地址与端口?

服务器常拥有确定的 IP 地址与端口以供客户端连接并访问,为了做到这一点,服务器上的服务程序需要自行确定需要使用的 IP 地址与端口,并使用 bind() 进行固定;但客户端在与服务器建立连接时,常由操作系统随机指定一个空闲的临时端口号,并使用主机设置的 IP 地址与服务器通信。显式地为客户端指定端口号可能会增加通信失败的可能性(因为端口可能被占用),因此客户端一般通信不需要绑定地址与端口,而是由操作系统动态分配。

int bind(int __fd, const struct sockaddr *__addr, socklen_t __len)

参数

  1. 已创建但未绑定的网络套接字文件描述符;
  2. 套接字连接信息结构体指针, 使用 IPv4 协议的套接字,初始化时使用结构体 struct sockaddr_in;使用 IPv6 协议的套接字,初始化时使用结构体 struct sockaddr_in6;其他情况请查看手册;传入的参数需用 struct sockaddr* 进行显式类型转换。
  3. 参数2中的结构体大小,按参数2传入的初始化结构体进行 sizeof() 运算得到。

返回值

绑定成功时,返回0;其他情况返回-1并设置 errno 值;

端口已被占用,将返回-1。

Info

sockaddr_in 结构体

位于 in.h

struct sockaddr_in {
    sa_family_t sin_family;
    in_port_t sin_port;			    /* Port number.  */
    struct in_addr sin_addr;		/* Internet address.  */
    /* Pad to size of `struct sockaddr'.  */
    unsigned char sin_zero[sizeof (struct sockaddr)
			   - __SOCKADDR_COMMON_SIZE
			   - sizeof (in_port_t)
			   - sizeof (struct in_addr)];
};

sin_family 指定协议族,常用 AF_INETAF_INET6sin_port 指定端口号,需要使用 htons() 将整型值转换。0~1023 端口常需要 root 才可绑定; sin_addr 指定 IP 地址,需要使用 inet_addr() 将字符串转换,或使用 INADDR_ANY 绑定到任何可用地址。

监听端口

仅面向连接的协议可用。

将套接字从主动连接状态设置为被动连接状态,等待客户端连接请求。

int listen(int __fd, int __n)

参数

  1. 已绑定的套接字文件描述符;
  2. 客户端等待连接队列的最多连接数,如果客户端请求连接时,服务程序未暂未响应,该请求会置入队列。若等待队列满,新请求将被拒绝,在 connect() 处返回-1并设置 errnoECONNREFUSED

返回值

成功启动监听后,返回0;其他情况返回-1并设置 errno 值。

接受连接请求

接受一个位于队列的连接请求,如果没有请求,将使线程等待。

int accept(int __fd, struct sockaddr *__restrict__ __addr, socklen_t *__restrict__ __addr_len)

参数

  1. 已启用监听状态的套接字文件描述符;
  2. 存储客户端连接信息的指针,将 struct sockaddr_in 显式转换为 struct sockaddr * 后传入;
  3. 参数2中的结构体大小,按参数2传入的初始化结构体进行 sizeof() 运算得到;但函数返回时此值会改变为实际使用的大小。

返回值

成功建立连接后,返回非负值的新套接字文件描述符用于通信;其他情况返回-1并设置 errno 值。

Info

[仅服务器] 要在 TCP 层拒绝后续的新连接,可使用 close()accept() 前直接关闭已启用监听状态的套接字;

要在连接后中断与某个主机的连接,可使用 close() 关闭与某个主机的连接。

获取已连接对向端的 IP 地址和端口

int getpeername(int __fd, struct sockaddr *__restrict__ __addr, socklen_t *__restrict__ __len)

参数

  1. 已建立连接的对向端套接字;
  2. 存储对向端信息的结构体指针;
  3. 目标结构体的大小。

返回值

获取成功时返回0;其他情况返回-1并设置 errno 值。

接收数据

接收指定网络套接字从目标主机收到的数据。

ssize_t recv(int __fd, void *__buf, size_t __n, int __flags)

参数

  1. 目标主机的网络套接字文件描述符;

  2. 存储数据的缓冲区起始地址;

  3. 存储数据的缓冲区大小;

  4. 接收数据的选项,可用选项有: MSG_PEEK 接收数据,但不从缓冲区移除;

    MSG_WAITALL 待全部数据到达时才停止等待,若连接关闭或发生错误仍会结束等待; MSG_DONTWAIT 非等待模式接收。

返回值

接收完成后,返回已接收数据的字节数;若客户端关闭了连接,返回0;其他情况返回-1并设置 errno 值。

Info

read() 也可用于接收网络套接字收到的数据,但 recv() 专为通过网络套接字接收数据而设计。

发送数据

通过指定网络套接字发送数据到目标主机。

ssize_t send(int __fd, const void *__buf, size_t __n, int __flags)

参数

  1. 目标主机的网络套接字文件描述符;

  2. 发送数据的缓冲区起始地址;

  3. 发送数据的缓冲区大小;

  4. 发送数据的选项,可用选项有:

    MSG_ODB 发送带外 (Out-of-Band) 数据,常用于紧急传输; MSG_DONTWAIT 非等待模式发送; MSG_NOSIGNAL 停用在连接断开时,操作系统向此进程发送 SIGPIPE 信号。

返回值

发送完成后,返回已发送数据的字节数;其他情况返回-1并设置 errno 值。

Info

write() 也可用于发送网络套接字收到的数据,但 send() 专为通过网络套接字发送数据而设计。