预备知识
网络字节序
在计算机内存中,当一个数据类型(比如一个 32 位的整数
int)占用多个字节时,就存在一个如何排列这些字节的顺序问题,这就是字节序
(Byte Order) 或 Endianness (端序)。
主要有两种字节序:
- 小端字节序
(Little-Endian):将数据的低位字节存放在内存的低地址处。这是目前绝大多数本地个人电脑(如
Intel, AMD 的 x86/x64 架构)使用的方式。
- 大端字节序
(Big-Endian):将数据的高位字节存放在内存的低地址处。这更符合人类的阅读习惯(从高位读到低位)。一些服务器、网络设备和早期的
PowerPC, MIPS 架构使用这种方式。
举个例子:存储 16 位的整数 0x1234 (十进制的 4660)
这里 0x12 是高位字节, 0x34 是低位字节。
小端存储:内存低地址 -> 0x34; 内存高地址 -> 0x12
大端存储:内存低地址 -> 0x12; 内存高地址 -> 0x34
如果只是本地数据的读取, 小端序或者大端序都可以自洽;
但是假如涉及到不同端序设备的网络通信,
双方的端序不同步会产生难以想象的后果. 为了避免这种混乱,TCP/IP
协议规定:所有在网络上传输的数据都必须统一使用大端字节序。这个统一的标准就被称为
网络字节序 (Network Byte Order)。
操作系统提供了一套标准的函数来在这两种字节序之间进行转换:
| 函数 | 功能 | 说明 | |——|——|——| | htons() | Host to Network Short |
将一个 16
位(short)的数从主机字节序转换到网络字节序。主要用于端口号。|
| htonl() | Host to Network Long | 将一个 32
位(long)的数从主机字节序转换到网络字节序。主要用于 IPv4
地址。| | ntohs() | Network to Host Short | 将一个 16
位的数从网络字节序转换到主机字节序。| | ntohl() | Network to Host Long |
将一个 32 位的数从网络字节序转换到主机字节序。|
核心原则:在发送数据前,所有非字符型数据(如端口号、IP地址整数值)都应该使用
hton() 系列函数转换为网络字节序(操作系统在调用 hton() 和
ntoh()
系列函数时,会自动根据主机的字节序进行正确的转换);在接收到数据后,应该使用
ntoh() 系列函数转换回主机字节序再使用。
IP 地址转换函数
我们已经知道,IP 地址(这里还是指传统的IPv4)有两种表示格式:
- 点分十进制字符串格式 (Presentation Format):方便人类阅读,例如
“192.168.10.1”。
- 整数格式 (Network Format):方便计算机处理,是一个 32
位的无符号整数,并且是网络字节序。
我们需要一组函数来在这两种格式之间进行转换, 这些函数定义在
<arpa/inet.h> 中:
- int inet_pton(int af, const char src, void dst);
- 功能:将字符串 (p) 格式的 IP 地址转换为网络 (n)
整数格式。pton 即 “IP to net”。
- af: 地址族,AF_INET 或 AF_INET6。
- src: 指向 IP 地址字符串的指针。
- dst: 指向转换后存放结果的内存地址(例如 struct in_addr
的地址)。
- 返回值为1代表成功; 0代表传入的src没有指向一个有效的IP地址;
-1代表失败
- const char inet_ntop(int af, const void src, char *dst,
socklen_t size);
- 功能:将网络 (n) 整数格式的 IP
地址转换为字符串 (p) 格式。ntop 即 “net to IP”。
sockaddr 数据结构
socket 编程接口(如 bind,
connect)需要被设计成通用的,以便能够处理多种不同的网络协议(IPv4, IPv6,
UNIX Domain Socket 等)。每种协议的地址结构都不同,例如:
- IPv4 地址需要:协议族、16 位端口号、32 位 IP 地址。
- IPv6 地址需要:协议族、16 位端口号、128 位 IP 地址,以及流信息和范围
ID。
如果为每种协议都设计一套独立的函数,如 bind_ipv4(),
bind_ipv6(),那将非常繁琐。
为了解决这个问题,Socket API 设计了一个“基类”结构体 struct
sockaddr。
1 2 3 4
| struct sockaddr { sa_family_t sa_family; char sa_data[14]; };
|
这是一个通用的、但内容模糊的结构体。我们几乎从不直接填充它。它的 sa_data
区域设计得足够大,可以容纳当时最常见的地址类型。
更常见的是, 我们使用专门用于 IPv4 的更清晰的 struct
sockaddr_in:
1 2 3 4 5 6 7 8 9
| #include <arpa/inet.h> struct sockaddr_in { sa_family_t sin_family; in_port_t sin_port; struct in_addr sin_addr; char sin_zero[8]; };
|
在实际使用过程中, 我们需要明确初始化sockaddr_in结构体,
根据客户端和服务端分别初始化如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <cstring>
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(9527);
if (inet_pton(AF_INET, "192.157.22.45", &server_addr.sin_addr) <= 0) { perror("inet_pton failed"); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <cstring>
struct sockaddr_in local_addr;
memset(&local_addr, 0, sizeof(local_addr));
local_addr.sin_family = AF_INET;
local_addr.sin_port = htons(9527);
local_addr.sin_addr.s_addr = htonl(INADDR_ANY);
|
网络套接字函数
socket模型创建流程

如图, 一个完整的通信流程会涉及到(至少)三个socket,其中客户端一个,
服务端两个. 客户端的比较直接, 我们主要关注服务端
具体流程是, 当服务器端调用socket()函数时会产生一个监听套接字
(Listening
Socket),而accept函数会阻塞监听套接字监听客户端连接,当有客户端过来连接的时候
该函数会返回一个新的套接字, 称作已连接套接字 (Connected
Socket)去和客户端连接
监听套接字 (Listening Socket) - 创建者:socket()
函数。 - 配置者:bind() 和 listen() 函数。 -
核心职责:作为一个“连接工厂”,它的唯一使命是在指定的 IP:Port
上监听并接收客户端发来的连接请求(TCP协议中的 SYN
包)。 -
数据交互:它从不参与应用层数据的收发。你永远不会对一个监听套接字使用
send() 或 recv() 函数。 -
生命周期:通常在服务器程序启动时创建,并一直存在,直到服务器程序关闭。对于一个服务器而言,一个端口上通常只有一个监听套接字。
已连接套接字 (Connected Socket) - 创建者:accept()
函数。 -
核心职责:作为与某一个特定客户端进行通信的专属通道。所有与该客户端的应用层数据(HTTP
请求、数据库查询、聊天消息等)都通过这个套接字进行 send() 和 recv()。 -
数据交互:它是数据传输的实际执行者。 -
生命周期:当一个客户端连接成功时被创建,当与这个客户端的通信结束时(客户端断开或服务器主动关闭),这个套接字就会被
close()
销毁。一个繁忙的服务器会频繁地创建和销毁成百上千个这样的套接字。
正是这种“一个监听,多个连接”的模式,构成了所有网络服务器能够高效服务于众多客户端的基础架构。
socket() 函数
socket套接字本意是插座,
意味着在数据通信过程中socket必须是成对出现的(客户端和服务端)
socket()
函数的作用是在操作系统内核中创建一个通信的端点,并返回一个指向该端点的文件描述符
(File Descriptor)。
你可以将这个过程理解为向操作系统申请一部“电话机”。这部“电话机”本身还不知道要打给谁(没有目标地址),也不知道自己的号码(没有绑定端口),但它是后续所有通信操作的基础。在类
Unix 系统(如
Linux)中,这个返回的文件描述符与其他文件(如磁盘文件、管道)的描述符一样,可以被
read(), write(), close() 等函数操作, 本质上也是一个文件。
函数原型 (Function
Prototype)及参数和返回值
socket() 函数通常在 C 语言的头文件 <sys/socket.h>
中定义。其标准原型如下:
1 2 3 4
| #include <sys/socket.h> #include <sys/types.h>
int socket(int domain, int type, int protocol);
|
这个函数接收三个整数类型的参数,并返回一个整数。
- domain:协议族 这个参数用于指定 Socket 使用的协议族 (Protocol
Family)。它决定了通信的地址格式和底层协议的范畴。
换句话说, 这个参数告诉操作系统你打算进行哪一类的通信,例如是基于 IPv4
的互联网通信,还是基于 IPv6,或者是仅限于本机内部的通信。
常用值(主要是AF_INET):
AF_INET (Address Family Internet): 这是最常用的值,表示使用 IPv4
协议族。网络地址将由 32 位的 IPv4 地址和 16 位的端口号组成。
AF_INET6: 表示使用 IPv6 协议族。网络地址将由 128 位的 IPv6 地址和
16 位的端口号组成。
AF_UNIX (或 AF_LOCAL): 用于本机内部进程间通信
(IPC)。它不使用网络协议,而是通过文件系统中的一个特殊文件(socket
文件)来进行数据交换,效率非常高。
- type:套接字类型 这个参数用于指定 Socket 的服务类型 (Service
Type),它决定了通信的语义和行为。使用这个参数需要明确你需要的通信方式是可靠的、面向连接的(像打电话),还是快速的、无连接的(像寄明信片)。
常用值: - SOCK_STREAM (Stream Socket): 流式套接字,
提供面向连接、可靠的、基于字节流的服务。它通常与 TCP
协议配合使用。 -
数据传输前必须先建立连接。它保证数据传输是有序的、无差错的、无重复的。数据像水流一样,没有边界,你发送
100 字节,对方可能一次性收到 100 字节,也可能先收到 40 字节再收到 60
字节。 - 适用场景:绝大多数应用,如网页浏览 (HTTP)、文件传输
(FTP)、邮件发送等。
- SOCK_DGRAM (Datagram Socket): 数据报套接字,
提供无连接、不可靠的、基于数据报的服务。它通常与
UDP 协议配合使用。
- 通信前不需要建立连接。每个数据包(数据报)都是独立的,有自己的目标地址。它不保证数据能到达,也不保证到达的顺序。
- 适用场景:对实时性要求高、能容忍少量丢包的场景,如在线游戏、视频直播、DNS
查询。
- protocol:具体协议
这个参数用于指定在前两个参数确定的协议族和套接字类型下,还想进一步使用的具体协议。因为在某些协议族中,可能有多种协议支持同一种套接字类型。这个参数允许你精确指定。
不过大部分情况下使用 0 即可:这是最常用的值。表示让操作系统根据
domain 和 type 的组合自动选择默认的协议, 等同于下列手动指定:
socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // 创建tcp的sock
socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); //
创建udp的sock
- 返回值 (Return Value)
成功时:返回一个非负整数,这个整数就是套接字描述符
(Socket Descriptor),也常被称为文件描述符。它是这个新创建的 Socket
的唯一标识。后续所有的 Socket 相关函数(如 bind(), connect(), listen()
等)都将使用这个描述符作为参数。
失败时:返回 -1。同时,全局变量 errno
会被设置为一个特定的错误码,以指示失败的原因。我们可以通过 perror()
函数或 strerror(errno)
来查看具体的错误信息。常见的错误原因包括:权限不足、协议不支持、系统资源耗尽等。
socket()函数简单示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| #include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <errno.h> #include <string.h>
int main() { int tcp_socket_fd; int udp_socket_fd;
tcp_socket_fd = socket(AF_INET, SOCK_STREAM, 0);
if (tcp_socket_fd == -1) { perror("Create TCP socket failed"); exit(EXIT_FAILURE); } printf("TCP socket created successfully! File Descriptor: %d\n", tcp_socket_fd);
udp_socket_fd = socket(AF_INET, SOCK_DGRAM, 0); if (udp_socket_fd == -1) { perror("Create UDP socket failed"); exit(EXIT_FAILURE); } printf("UDP socket created successfully! File Descriptor: %d\n", udp_socket_fd);
return 0; }
|
bind()函数
bind 函数的作用是给套接字“绑定”一个地址。在我们之前的比喻中,socket()
函数只是创建了一个“电话机”,而 bind
函数就是向电信局申请一个具体的电话号码(IP 地址 +
端口号)并分配给这部电话机。
对于服务器来说,这是一个必须的步骤,因为客户端必须知道服务器的“地址”才能发起连接。
函数原型及参数解释如下:
1 2
| #include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
|
- int sockfd: 由 socket()
函数返回的套接字文件描述符。 - const struct sockaddr
addr:
由const可知是传入参数, 是一个指向 sockaddr
结构体的指针。这正是我们之前讨论过的“预备知识”的应用。我们通常会创建一个
struct sockaddr_in (for IPv4) 变量,填充好 sin_family, sin_port (端口)
和 sin_addr (IP 地址),然后将其指针强制转换为 (struct sockaddr )
再传递给 bind。 - socklen_t addrlen: addr
指向的地址结构体的确切大小,通常使用 sizeof(struct sockaddr_in)。 -
返回值: 成功返回 0; 失败返回 -1,并设置全局变量
errno。常见的失败原因包括:端口已被占用 (EADDRINUSE)、没有权限绑定该地址
(EACCES)等。
需要注意的是, bind 是服务器端专用的函数(客户端通常不需要),它在
socket() 创建套接字之后、listen() 开始监听之前被调用。
listen() 函数
listen
函数的作用是让套接字进入“被动监听”模式以及设置最多同时连接数。一个普通的套接字(由
socket()
创建)既可以主动发起连接(作为客户端),也可以被动接收连接(作为服务器)。一旦调用
listen,这个套接字就从一个“主动”套接字转变为一个“被动”的、专门用于接收连接请求的监听套接字。
在我们之前的比喻中,这相当于把公司的总机电话设置为“等待来电”状态,并告诉交换机系统,可以开始向这个号码派发来电了。
函数原型及参数解释:
1 2
| #include <sys/socket.h> int listen(int sockfd, int backlog);
|
- int sockfd:: 已经被 bind()
绑定了地址的套接字文件描述符。 - int
backlog:一个非常重要的参数,它规定了内核为这个监听套接字维护的“
待处理连接队列”的最大长度。当服务器非常繁忙,来不及
accept()
新的连接时,新来的连接请求会先被放入这个队列中排队。如果队列已满,新的客户端连接请求可能会被拒绝。这个值的大小需要根据服务器的负载能力来设置,一个常见的值是
SOMAXCONN (一个由系统定义的较大值)。 - 返回值: 成功返回 0; 失败返回
-1,并设置 errno。
同样, listen 也是服务器端专用的函数,在 bind() 之后、accept()
之前被调用。
accept() 函数
accept
函数是服务器从“待处理连接队列”中取出一个连接请求,并创建一个全新的套接字来与该客户端通信。accept
接收的 sockfd 是监听套接字,而它返回的是一个全新的已连接套接字。
这是一个阻塞函数:如果队列中没有已完成的连接,程序会在这里暂停,直到有客户端连接进来为止。
函数原型及参数解释:
1 2
| #include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
|
- int sockfd:
正在监听的那个监听套接字的文件描述符。 - struct sockaddr
addr
(可选): 没有const, 因此很大可能(实际上也就是)传出参数, 是一个指向
sockaddr
结构体的指针,用于接收客户端的地址信息。当函数成功返回时,内核会把发起连接的客户端的
IP 和端口填充到这个结构体中。如果你不关心客户端的地址,可以把它设为
NULL。 - socklen_t addrlen (可选): 一个指向 socklen_t
变量的指针。在调用前,你需要把它指向的变量设置为 addr
指向的缓冲区的最大长度 (sizeof(struct
sockaddr_in))。函数返回后,这个变量的值会变为客户端地址结构体的实际长度。如果
addr 是 NULL,这个参数也应为 NULL。 - 返回值;
成功返回一个新的非负整数,这个整数就是
新创建的已连接套接字的文件描述符。后续与该客户端的所有通信(send/recv)都将使用这个新的描述符;
失败返回 -1,并设置 errno。
accept 也是服务器端专用的函数,通常在 listen()
之后的一个主循环中被反复调用。
connect() 函数
connect
函数由客户端调用,用于向指定的服务器地址发起一个主动的连接请求。
这个函数会触发 TCP
协议的三次握手过程。它也是一个阻塞函数,在三次握手成功建立连接、或者超时/失败之前,程序会一直等待。
函数原型及参数解释:
1 2
| #include <sys/socket.h> int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
|
- int sockfd: 客户端自己的、由
socket() 创建的套接字文件描述符。 - const struct sockaddr *addr:
传入参数, 一个指向 sockaddr
结构体的指针,里面包含了
服务器的 IP
地址和端口号。客户端必须准确地填充这个结构体,才能找到正确的服务器。 -
socklen_t addrlen: addr 指向的服务器地址结构体的确切大小。 - 返回值:
成功返回 0。此时 TCP 连接已成功建立; 失败返回 -1,并设置
errno。常见的失败原因包括:服务器拒绝连接 (ECONNREFUSED)、网络不可达
(ENETUNREACH)、连接超时 (ETIMEDOUT)等。
connect 是客户端专用的函数,在 socket() 创建套-接字之后被调用。
Client-Server 示例
下面是一个简单的 TCP 客户端-服务器示例,展示了如何使用上述的 socket
函数进行基本的网络通信。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
|
#include <iostream> #include <cstdio> #include <cstring> #include <cstdlib> #include <unistd.h> #include <netdb.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h>
void HandleRequest(int client_sockfd);
int main(int argc, char *argv[]) { if (argc != 2) { std::cout << "Using: ./server port\nExample: ./server 5005\n\n"; return -1; }
int listen_sockfd = socket(AF_INET, SOCK_STREAM, 0); if (listen_sockfd == -1) { perror("socket"); return -1; }
struct sockaddr_in serv_addr; memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port = htons(atoi(argv[1])); if (bind(listen_sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) != 0) { perror("bind"); close(listen_sockfd); return -1; }
if (listen(listen_sockfd, 5) != 0) { perror("listen"); close(listen_sockfd); return -1; } std::cout << "Server is listening on port " << argv[1] << "..." << std::endl;
while (true) { struct sockaddr_in client_addr; socklen_t len = sizeof(client_addr); int client_sockfd = accept(listen_sockfd, (struct sockaddr *)&client_addr, &len); if (client_sockfd < 0) { perror("accept"); continue; }
char ipstr[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &client_addr.sin_addr, ipstr, sizeof(ipstr)); std::cout << "Client " << ipstr << " connected." << std::endl;
HandleRequest(client_sockfd); }
close(listen_sockfd); return 0; }
void HandleRequest(int client_sockfd) { char buffer[1024]; while (true) { memset(buffer, 0, sizeof(buffer)); ssize_t bytes_received = read(client_sockfd, buffer, sizeof(buffer) - 1);
if (bytes_received > 0) { std::cout << "Received from client: " << buffer << std::endl; for (int i = 0; buffer[i]; ++i) { buffer[i] = toupper(buffer[i]); }
if (write(client_sockfd, buffer, strlen(buffer)) <= 0) { perror("write"); break; } std::cout << "Sent to client: " << buffer << std::endl; } else if (bytes_received == 0) { std::cout << "Client disconnected." << std::endl; break; } else { perror("read"); break; } }
close(client_sockfd); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
|
#include <iostream> #include <cstdio> #include <cstring> #include <cstdlib> #include <unistd.h> #include <netdb.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h>
int main(int argc, char *argv[]) { if (argc != 3) { std::cout << "Using: ./client ip port\nExample: ./client 127.0.0.1 5005\n\n"; return -1; }
int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd == -1) { perror("socket"); return -1; }
struct sockaddr_in serv_addr; memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(atoi(argv[2])); if (inet_pton(AF_INET, argv[1], &serv_addr.sin_addr) <= 0) { perror("inet_pton"); close(sockfd); return -1; }
if (connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) != 0) { perror("connect"); close(sockfd); return -1; } std::cout << "Connected to server " << argv[1] << ":" << argv[2] << std::endl;
char buffer[1024]; std::string input; std::cout << "Enter a message (or 'exit' to quit): "; while (getline(std::cin, input) && input != "exit") { if (write(sockfd, input.c_str(), input.length()) <= 0) { perror("write"); break; }
memset(buffer, 0, sizeof(buffer)); ssize_t bytes_received = read(sockfd, buffer, sizeof(buffer) - 1); if (bytes_received > 0) { std::cout << "Server response: " << buffer << std::endl; } else if (bytes_received == 0) { std::cout << "Server disconnected." << std::endl; break; } else { perror("read"); break; } std::cout << "\nEnter a message (or 'exit' to quit): "; }
close(sockfd); std::cout << "Connection closed." << std::endl;
return 0; }
|
端口复用
在开发和测试服务器程序时,肯定会遇到一个经典问题:第一次启动了服务器,它成功
bind() 到 5005 端口并开始 listen(); 接着通过 Ctrl+C 强制关闭了服务器;
然后立刻尝试重新启动服务器。
此时,bind() 函数调用失败,程序打印出错误信息:bind: Address already
in
use。我们不得不等待几十秒甚至几分钟后,才能再次成功启动服务器。这个问题在开发调试阶段非常影响效率,在生产环境中也可能导致服务中断时间变长。
这个现象的根本原因在于 TCP 协议的一个重要状态:TIME_WAIT。
回想TCP 四次挥手:当一个 TCP
连接被关闭时(例如服务器或客户端程序退出),主动关闭连接的一方会进入
TIME_WAIT 状态。这个状态会持续一段时间,通常是 2 * MSL (Maximum Segment
Lifetime,报文最大生存时间),在 Linux 系统上一般是 60 秒。
当我们的服务器程序关闭后,它所使用的套接字(绑定了例如
127.0.0.1:5005)就进入了 TIME_WAIT
状态。在此期间,操作系统认为这个端口仍然是“被占用的”,因此不允许任何新的套接字再次
bind() 到完全相同的地址和端口上。
为了解决这个问题,Socket API
提供了一个非常有用的选项:SO_REUSEADDR。
在设置 SO_REUSEADDR 选项后,它会告诉操作系统内核:“请允许我
bind()到一个正处于 TIME_WAIT 状态的端口”。它放宽了 bind
函数的校验规则,使得服务器可以在关闭后立刻重启,绕过 TIME_WAIT 状态对
bind 的限制。
这对于需要高可用性和快速重启的服务器应用程序来说,是必须设置的一个选项。
要启用端口复用,我们需要在 bind() 函数被调用之前,使用 setsockopt()
函数来设置监听套接字的属性, 其函数原型如下:
1 2
| #include <sys/socket.h> int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
|
- int sockfd:
要设置的套接字文件描述符(这里是我们的 listen_sockfd)。 - int level:
选项所在的协议层。对于端口复用,应设置为
SOL_SOCKET,表示在通用套接字层进行设置。 - int optname:
选项的名称。这里我们使用 SO_REUSEADDR。 - const void *optval:
一个指向变量的指针,该变量包含了我们想设置的选项的值。对于开关型选项
SO_REUSEADDR,我们通常用一个
值为 1 的 int
变量来表示“开启”。 - socklen_t optlen: optval
指向的变量的大小,即 sizeof(int)。
下面是一个启用端口复用的示例代码片段,展示了如何在创建监听套接字后、调用
bind() 之前设置 SO_REUSEADDR 选项:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
int listen_sockfd = socket(AF_INET, SOCK_STREAM, 0); if (listen_sockfd == -1) { perror("socket"); return -1; }
int opt = 1; if (setsockopt(listen_sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) != 0) { perror("setsockopt"); close(listen_sockfd); return -1; }
struct sockaddr_in serv_addr;
if (bind(listen_sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) != 0) { }
|
总之, 端口复用 (SO_REUSEADDR) 是一个健壮的 TCP
服务器程序必备的特性。它通过 setsockopt
函数进行设置,允许程序重新绑定到处于 TIME_WAIT
状态的端口,从而解决了服务器因异常关闭而无法立即重启的问题,极大地提高了开发效率和服务的可用性。