3、TCP服务端实现:创建服务端socket、绑定地址、监听、接受连接、收发数据
好,咱们今天来聊聊TCP服务端的实现。这部分可以说是网络编程的基石,你想想看,几乎所有互联网服务——从Web服务器到聊天软件——底层都是这套流程。
我个人习惯把TCP服务端编程总结为五个步骤:创建socket、绑定地址、监听、接受连接、收发数据。这五个步骤环环相扣,少一步都不行。我在项目中遇到过不少新手,上来就写收发数据,结果socket都没创建好,折腾半天找不到问题。
3.1 创建服务端socket
第一步,用socket()函数创建一个套接字。服务端和客户端的创建方式其实一样,区别在于后续的操作。
#include <sys/socket.h>
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
这里AF_INET表示IPv4协议族,SOCK_STREAM表示流式套接字——说白了就是TCP。第三个参数0表示让系统自动选择协议。
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
3.2 绑定地址
创建完socket,接下来要把它和一个地址绑定起来。地址包含IP和端口,告诉操作系统:这个socket监听哪个IP的哪个端口。
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有网卡
server_addr.sin_port = htons(8888); // 端口8888
if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("bind");
close(sockfd);
exit(EXIT_FAILURE);
}
嗯,这里要注意几个细节:
INADDR_ANY表示监听本机所有IP地址。如果你只想监听某个特定IP,可以换成具体的地址。htonl()和htons()是字节序转换函数。网络字节序是大端,而x86机器是小端,不转换会出问题。- 端口号建议用1024以上的,避免权限问题。1024以下需要root权限。
netstat -tlnp | grep 8888检查一下。我曾经在调试时忘了关上一个服务端进程,结果bind一直返回-1,查了半天才发现。
3.3 监听
绑定成功后,调用listen()让socket进入监听状态。这个函数告诉内核:准备好接受连接请求了。
if (listen(sockfd, 10) == -1) {
perror("listen");
close(sockfd);
exit(EXIT_FAILURE);
}
第二个参数是backlog,表示连接请求队列的最大长度。什么意思呢?当多个客户端同时连接时,来不及处理的请求会先排队。backlog就是这个队列的大小。
| backlog值 | 说明 |
|---|---|
| 5 | 小型服务,并发量低 |
| 10 | 一般服务,够用 |
| 128 | 高并发场景 |
我个人习惯设成10或20。设太大其实没意义,因为真正的并发处理能力取决于后续的accept和业务逻辑。
3.4 接受连接
监听之后,用accept()接受客户端的连接。这个函数会阻塞,直到有客户端连上来。
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
int client_fd = accept(sockfd, (struct sockaddr*)&client_addr, &addr_len);
if (client_fd == -1) {
perror("accept");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("客户端连接成功,IP: %s, 端口: %d\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port));
accept返回一个新的socket描述符client_fd,后续的收发数据都用这个新socket。原来的sockfd继续监听,等待下一个连接。
3.5 收发数据
连接建立后,用read()和write()收发数据。这两个函数和文件操作一样,因为Linux下一切皆文件。
char buffer[1024];
int n;
// 接收数据
n = read(client_fd, buffer, sizeof(buffer) - 1);
if (n > 0) {
buffer[n] = '\0';
printf("收到: %s\n", buffer);
} else if (n == 0) {
printf("客户端关闭连接\n");
} else {
perror("read");
}
// 发送数据
const char *response = "Hello, client!";
write(client_fd, response, strlen(response));
这里有几个要点:
read()返回0表示对方关闭了连接,这时候要记得close(client_fd)。- 返回-1表示出错,需要处理错误。
- buffer要留一个字节给'\0',方便打印字符串。
send()和recv()。它们多了flags参数,可以控制更多行为。比如MSG_WAITALL标志可以保证读取指定长度的数据。
3.6 完整代码示例
把上面这些串起来,就是一个完整的TCP服务端:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main() {
// 1. 创建socket
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
// 设置端口复用
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 2. 绑定地址
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(8888);
if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("bind");
close(sockfd);
exit(EXIT_FAILURE);
}
// 3. 监听
if (listen(sockfd, 10) == -1) {
perror("listen");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("服务端启动,监听端口8888...\n");
// 4. 接受连接
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
int client_fd = accept(sockfd, (struct sockaddr*)&client_addr, &addr_len);
if (client_fd == -1) {
perror("accept");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("客户端连接: %s:%d\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port));
// 5. 收发数据
char buffer[1024];
int n = read(client_fd, buffer, sizeof(buffer) - 1);
if (n > 0) {
buffer[n] = '\0';
printf("收到: %s\n", buffer);
write(client_fd, "已收到你的消息", strlen("已收到你的消息"));
}
close(client_fd);
close(sockfd);
return 0;
}
3.7 核心流程总结
为了让你更直观地理解整个流程,我画了一张图:
这张图把整个流程串起来了。你注意看,socket()创建的是监听套接字,accept()返回的是通信套接字。这两个角色不同,千万别搞混。
好了,TCP服务端的五个步骤就讲完了。代码虽然简单,但每个细节都值得推敲。你写代码时,建议一步步来,每完成一步就打印个日志,这样出了问题也好定位。
公众号:蓝海资料掘金营,微信deep3321