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)参数
-
传输要使用的协议族,可用的协议族有:
AF_INETIPv4 协议;AF_INET6IPv6 协议;AF_UNIX / AF_LOCAL本机通信;AF_PACKET数据链路层原始帧通信;AF_NETLINK内核与用户空间通信。 -
套接字的数据传输方式,可用的传输方式有:
SOCK_STREAM有连接的、流式数据传输(TCP 使用);SOCK_DGRAM无连接的数据报传输(UDP 使用); [需要 root]SOCK_RAW不使用传输层协议,直接到达网络层;SOCK_SEQPACKET有序分组套接字; 同时允许与以下选项共用:SOCK_NONBLOCK停用等待;SOCK_CLOEXEC运行 exec 系列函数时,关闭套接字。 -
协议,设为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)参数
- 已创建但未绑定的网络套接字文件描述符;
- 套接字连接信息结构体指针,
使用 IPv4 协议的套接字,初始化时使用结构体
struct sockaddr_in;使用 IPv6 协议的套接字,初始化时使用结构体struct sockaddr_in6;其他情况请查看手册;传入的参数需用struct sockaddr*进行显式类型转换。 - 参数2中的结构体大小,按参数2传入的初始化结构体进行
sizeof()运算得到。
返回值
绑定成功时,返回0;其他情况返回-1并设置 errno 值;
端口已被占用,将返回-1。
Info
sockaddr_in 结构体
位于
in.hstruct 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_INET和AF_INET6;sin_port指定端口号,需要使用htons()将整型值转换。0~1023 端口常需要 root 才可绑定;sin_addr指定 IP 地址,需要使用inet_addr()将字符串转换,或使用INADDR_ANY绑定到任何可用地址。
监听端口
仅面向连接的协议可用。
将套接字从主动连接状态设置为被动连接状态,等待客户端连接请求。
int listen(int __fd, int __n)参数
- 已绑定的套接字文件描述符;
- 客户端等待连接队列的最多连接数,如果客户端请求连接时,服务程序未暂未响应,该请求会置入队列。若等待队列满,新请求将被拒绝,在
connect()处返回-1并设置errno为ECONNREFUSED。
返回值
成功启动监听后,返回0;其他情况返回-1并设置 errno 值。
接受连接请求
接受一个位于队列的连接请求,如果没有请求,将使线程等待。
int accept(int __fd, struct sockaddr *__restrict__ __addr, socklen_t *__restrict__ __addr_len)参数
- 已启用监听状态的套接字文件描述符;
- 存储客户端连接信息的指针,将
struct sockaddr_in显式转换为struct sockaddr *后传入; - 参数2中的结构体大小,按参数2传入的初始化结构体进行
sizeof()运算得到;但函数返回时此值会改变为实际使用的大小。
返回值
成功建立连接后,返回非负值的新套接字文件描述符用于通信;其他情况返回-1并设置 errno 值。
Info
[仅服务器] 要在 TCP 层拒绝后续的新连接,可使用
close()在accept()前直接关闭已启用监听状态的套接字;要在连接后中断与某个主机的连接,可使用
close()关闭与某个主机的连接。
获取已连接对向端的 IP 地址和端口
int getpeername(int __fd, struct sockaddr *__restrict__ __addr, socklen_t *__restrict__ __len)参数
- 已建立连接的对向端套接字;
- 存储对向端信息的结构体指针;
- 目标结构体的大小。
返回值
获取成功时返回0;其他情况返回-1并设置 errno 值。
接收数据
接收指定网络套接字从目标主机收到的数据。
ssize_t recv(int __fd, void *__buf, size_t __n, int __flags)参数
-
目标主机的网络套接字文件描述符;
-
存储数据的缓冲区起始地址;
-
存储数据的缓冲区大小;
-
接收数据的选项,可用选项有:
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)参数
-
目标主机的网络套接字文件描述符;
-
发送数据的缓冲区起始地址;
-
发送数据的缓冲区大小;
-
发送数据的选项,可用选项有:
MSG_ODB发送带外 (Out-of-Band) 数据,常用于紧急传输;MSG_DONTWAIT非等待模式发送;MSG_NOSIGNAL停用在连接断开时,操作系统向此进程发送SIGPIPE信号。
返回值
发送完成后,返回已发送数据的字节数;其他情况返回-1并设置 errno 值。
Info
write()也可用于发送网络套接字收到的数据,但send()专为通过网络套接字发送数据而设计。