一、引言
本文介绍一个Linux 系统下的TCP服务端程序,功能如下:
1.绑定 IP + 端口
2.监听客户端连接
3.接受一个客户端连接
4.接收客户端发来的消息
5.回复固定消息:"服务端已收到你的消息"
6.支持 优雅退出(kill 进程不会直接崩)
本文代码参考连接:game-team-server/net · 2401_84149564/网络团队竞技游戏服务端 - AtomGit | GitCode
VS Code中Linux C++代码运行截图:
服务端:

客户端:

二、需要用到的头文件
cpp
#include<iostream> // C++ 输入输出(本文虽然没有用到,但保留也没问题)
#include<sys/socket.h> // socket、bind、listen、accept、send、recv
#include<netinet/in.h> // sockaddr_in 结构体、htons 字节序转换
#include<arpa/inet.h> // inet_pton IP字符串转网络字节序
#include<signal.h> // 信号处理(优雅退出用)
#include<unistd.h> // close()、sleep()
#include<cstdlib> // atoi() 字符串转数字
#include<cassert> // 断言,检查函数调用是否成功
#include<cstdio> // printf 打印
#include<string> // C++ string 类
#include<cstring> // strlen、memset 等字符串函数
三、全局变量 & 信号处理
1.全局退出标记
作用:收到退出信号时,让循环结束,实现优雅退出。
cpp
static bool stop=false;
2.信号处理函数
当我们在终端输入 kill 进程号,系统会发送 SIGTERM 信号
这个函数会被触发,把 stop = true
服务端就会跳出循环,安全关闭 socket,不会崩溃
cpp
static void handle_term(int sig){
stop=true;
}
四、主函数
1.注册信号
告诉系统:收到终止信号时,调用 handle_term。
cpp
signal(SIGTERM,handle_term);
2.检查启动参数
必须传入 3 个参数:IP、端口、backlog
例如:./testlisten 127.0.0.1 8888 5
cpp
if(argc<=3){
printf("usage: %s ip_address port_number backlog\n",argv[0]);
return 1;
}
3.创建 TCP 套接字
PF_INET:IPv4 协议
SOCK_STREAM:TCP 协议
返回值:文件描述符 fd(相当于插座编号)
cpp
int sock=socket(PF_INET,SOCK_STREAM,0);
assert(sock>=0);
4.端口复用(服务端必备)
解决:重启服务时提示 Address already in use
高并发服务端必加!
cpp
int opt = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
5.绑定 IP 和端口
sockaddr_in:IPv4 地址结构体
inet_pton:把字符串 IP 转为网络格式
htons(port):把端口转为网络字节序(大端序)
cpp
struct sockaddr_in address{};
address.sin_family=AF_INET;
inet_pton(AF_INET,ip,&address.sin_addr);
address.sin_port=htons(port);
把 socket 和 IP: 端口 绑定。
cpp
bind(sock,(struct sockaddr*)&address,sizeof(address));
6. 开始监听
让 socket 从 "主动" 变 "被动"
开始等待客户端连接
backlog:半连接队列长度(TCP 三次握手队列)
cpp
ret=listen(sock,backlog);
7. 接受客户端连接
阻塞函数:没有客户端连接时,会卡在这里等
返回值:新的文件描述符 client_fd
以后和客户端收发数据全靠它
cpp
int client_fd = accept(sock, (struct sockaddr*)&client_addr, &client_len);
打印客户端的 IP 和端口号。
cpp
printf("客户端IP:%s 端口:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
8.循环收发消息(业务逻辑)
**recv:**从客户端读取数据,存到 buf 里
(1)n > 0:收到 n 字节
(2)n = 0:客户端断开
(3)n < 0:出错
**buf[n] = '\0':**给字符串加结束符,避免乱码。
**send:**向客户端发送固定回复。
cpp
while(!stop){
char buf[1024];
ssize_t n = recv(client_fd, buf, sizeof(buf)-1, 0);
if(n <= 0){
printf("客户端断开连接\n");
break;
}
buf[n] = '\0';
printf("收到客户端数据:%s\n", buf);
send(client_fd, "服务端已收到你的消息", strlen("服务端已收到你的消息"), 0);
}
9.关闭资源 & 退出
先关客户端 socket
再关监听 socket
安全释放资源
cpp
close(client_fd);
close(sock);
printf("服务端优雅退出\n");
五、完整运行流程
1.启动服务端:./testlisten 0.0.0.0 8888 5
2.服务端执行:socket → bind → listen
3.阻塞在 accept 等待客户端
4.客户端 telnet 127.0.0.1 8888 连接
5.accept 返回,拿到 client_fd
6.进入循环,recv 等待客户端发消息
7.客户端发消息 → 服务端打印 → 回复消息
8.客户端断开 → 循环结束
9.关闭 fd → 服务端退出
六、代码的局限性
1.只能处理一个客户端
2.是 阻塞模型,效率低
3.没有处理多连接
4.没有处理异常崩溃
5.没有处理粘包
七、本文的问答题目(模拟面试官提问 + 标准回答)
1. 请简述你这段 TCP 服务端完整流程
标准答案:
socket()创建 TCP 套接字;setsockopt设置端口复用,解决重启端口占用问题;bind()绑定服务器 IP 与端口;listen()将套接字转为监听状态,开始监听客户端连接;accept()阻塞接受客户端连接,得到客户端通信文件描述符;recv/send循环收发数据,实现服务端回声;- 客户端断开或收到退出信号,
close()关闭套接字,优雅退出。
2. 什么是套接字 socket?
标准答案:
socket 是操作系统提供的网络通信接口 ,本质是一个文件描述符 fd,Linux 一切皆文件,网络通信也通过文件描述符读写。服务端监听套接字、客户端连接套接字,都是不同的 fd。
3. 服务端两个 fd 的区别(高频)
代码里有 两个 fd :sock(监听 fd)、client_fd(通信 fd)
标准答案:
sock监听套接字:只负责监听、接受客户端连接,不负责数据收发;client_fd连接套接字:专门和已经连接成功的客户端收发数据;- 每来一个新客户端,
accept就会返回一个新的client_fd。
4. 启动参数 ip port backlog 分别含义
bash
./testlisten 0.0.0.0 8888 5
标准答案:
1.ip:服务端绑定监听的 IP127.0.0.1 本机回环,仅本机可连;0.0.0.0 监听所有网卡,全网可连。
2.port:服务端端口号
3.backlog:TCP 半连接队列长度,存放三次握手未完成的连接数。
5. socket(PF_INET, SOCK_STREAM, 0) 三个参数含义
标准答案:
1.第一个 PF_INET:使用 IPv4 网络协议;
2.第二个 SOCK_STREAM:使用 TCP 流式协议(面向连接、可靠传输);
3.第三个参数 0:默认协议。
6. 为什么要用 htons() 转换端口?
标准答案:
- 主机字节序(小端序):电脑 CPU 存储数据格式;
- 网络字节序 (大端序):TCP/IP 网络统一传输格式;不同主机大小端不一样,网络必须统一大端。
htons:host to network short,主机端口转网络字节序 。同理htonl转 IP,ntohs/ntohl网络转回主机。
7. inet_pton 和 inet_ntoa 作用
标准答案:
1.inet_pton:字符串 IP → 网络二进制 IP,用于服务端绑定;
2.inet_ntoa:网络二进制 IP → 字符串 IP,用于打印客户端 IP。
8. bind 作用?为什么要绑定?
标准答案:
把创建好的 socket 文件描述符,和指定的 IP、端口号绑定。不绑定的话,内核无法知道数据要交给哪个进程,客户端无法连接服务端。
9. listen 做了什么?为什么 listen 之后才能接受连接?
标准答案:
listen 将主动套接字转为被动监听套接字 ,内核开始维护连接队列,等待客户端发起 TCP 三次握手。调用 listen 之前,socket 只是普通文件描述符,不具备服务端监听能力。
10. accept 函数详解(超高频)
cpp
int client_fd = accept(sock, ...);
标准答案:
accept是阻塞函数,没有客户端连接时,线程会一直阻塞等待;- 从
listen的已完成连接队列取出一个连接; - 返回一个新的文件描述符 client_fd,用于后续和客户端通信;
- 第一个参数依旧是监听套接字 sock,不是 client_fd。
11. recv 和 send 返回值含义
标准答案:
ssize_t n = recv(...)
n > 0:成功读取到n字节数据;n == 0:客户端正常关闭连接(对方 close);n < 0:网络异常、出错。
send 返回成功发送的字节数,失败返回 -1。
12. 代码里 buf[n] = '\0'; 为什么要加这一句?
标准答案:
recv 只拷贝原始字节数据,不会自动添加字符串结束符 \0。
不加结束符,printf 打印会读到缓冲区脏数据,出现乱码。
13. 为什么要加端口复用?你代码这行意义
cpp
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
标准答案:
服务端程序关闭后,端口会进入 TIME_WAIT 状态 (一般持续 1~2 分钟),此时直接重启服务会报错 Address already in use 地址被占用。开启 SO_REUSEADDR 端口复用,可以立即重新绑定端口,开发调试必备,所有服务端必加。
14. 你代码的 SIGTERM 信号优雅退出有什么意义?
标准答案:
- 暴力
kill -9杀死进程会直接释放资源,容易造成端口残留; SIGTERM是温和终止信号,程序可以主动执行逻辑:结束循环、关闭 socket、释放资源,优雅安全退出,避免资源泄漏。stop全局标志位用于安全跳出主循环。
15. kill 与 kill -9 区别
标准答案:
kill pid:发送SIGTERM默认信号,程序可以捕获、优雅退出;kill -9 pid:强制杀死进程,无法捕获,内核直接回收资源,服务端不推荐。
16. 客户端和服务端完整 TCP 三次握手过程
结合你代码回答版
- 客户端发起 SYN 连接请求;
- 服务端响应 SYN+ACK;
- 客户端回复 ACK;三次握手完成,连接建立,
accept解除阻塞返回。
17. backlog 参数到底是什么?
标准答案 backlog 代表 已完成连接队列(全连接队列)最大长度 。存放已经完成三次握手、等待 accept 取走的连接。连接超过该数量,新连接会被内核拒绝。
18. TCP 粘包问题(必问,结合代码缺陷)
标准答案:
TCP 是字节流协议,无数据边界,内核会合并多次小数据包一起发送。你当前代码一次收发单行消息暂时没体现,但连续高频发送就会出现粘包,导致读取数据错乱。解决方案:自定义数据包协议(包头 + 包长)、定长包、分隔符。
19. TCP 四次挥手(断开连接过程)
客户端 close 断开时,双方四次挥手断开连接,服务端 recv 返回 0 识别断开。
20. 分析当前阻塞模型服务端缺点
满分标准答案:
- 单客户端阻塞模型 ,同一时间只能处理一个客户端连接 ;
accept阻塞、recv也阻塞,第一个客户端连接后,新客户端无法接入; - 纯阻塞 IO,没有高并发能力,性能极低;
- 没有处理网络异常、信号异常容错;
- 没有解决 TCP 粘包问题;
- 只支持简单字符串收发,无自定义业务协议。
21. 如何让服务端支持多客户端同时连接?
按学习顺序回答(最标准)
- 多进程模型 :每一个客户端
fork子进程处理; - 多线程模型:每一个连接创建一个线程处理;
- I/O 多路复用 :
select / poll / epoll,单线程管理大量连接; - Reactor 反应堆模型(大厂游戏服务端主流)。
22. 为什么游戏服务端首选 epoll,而不是多线程?
标准答案:
多线程线程创建销毁开销大、线程切换开销大、线程安全问题多;
epoll 是 Linux 内核实现的 I/O 多路复用,事件驱动,高并发、低开销,百万连接无压力,王者荣耀等手游服务端底层全部基于 epoll。
23. 阻塞 IO 是什么?
accept、recv 没有事件就一直卡住等待,不返回,占用线程。该代码就是典型阻塞 IO 模型。
24. 127.0.0.1 和 0.0.0.0 区别
标准答案
127.0.0.1:本机回环地址,仅本机进程可访问,外部电脑、Windows 无法连接;0.0.0.0:通配地址,监听本机所有网卡,局域网、外网设备均可连接。
25. send 发送成功就代表客户端一定收到了吗?
标准答案:
不是 。send 返回仅代表数据拷贝到内核发送缓冲区成功,不代表对方已经接收。TCP 底层滑动窗口、内核缓冲区负责后续传输。
26. 基于这个基础服务端,如何扩展成游戏战斗服务端?
满分回答:
- 封装自定义消息协议:消息头(长度、指令号)+ 消息体;
- 指令区分:进入房间、移动、技能、心跳、帧同步数据;
- 改用 epoll 高并发框架,支持大量玩家同时在线;
- 增加房间管理、玩家数据管理、帧同步逻辑;
- 增加心跳保活、断线重连、风控校验。
27. 服务端如何检测客户端掉线?
标准答案:
recv返回值为 0 或负数;- 增加心跳包机制,客户端定时发心跳,服务端超时未收到则判定掉线,回收资源。
八、简答题速记版
- TCP 服务端五步:socket → bind → listen → accept → read/write/close
- 监听 fd 负责接连接,通信 fd 负责收发数据。
- htons 大小端转换,网络统一大端。
- SO_REUSEADDR 端口复用,解决重启占用。
- recv 返回 0 代表客户端关闭,>0 为字节数,<0 出错。
- 阻塞模型缺点:单连接、性能差、无法高并发。
- 高并发优化路线:多线程 → select/poll → epoll → Reactor
- 优雅退出:捕获 SIGTERM 信号,安全关闭 fd,防止资源泄漏。
九、总结
本文介绍了一个基于Linux的TCP服务端程序实现,主要包括以下内容:
(1)程序功能:绑定IP端口、监听连接、收发消息并回复固定响应,支持优雅退出;
(2)关键实现:使用socket、bind、listen等系统调用,处理信号实现优雅关闭,设置端口复用解决地址占用问题;
(3)运行流程:完整演示了从启动服务到处理客户端连接的数据交互过程;
(4)局限性分析:指出当前单线程阻塞模型的不足,如无法处理多连接、性能低下等问题;
(5)扩展知识:提供了TCP相关面试题的解答,包括套接字原理、网络字节序、高并发优化方案等。
文章为网络编程初学者提供了完整的服务端实现范例,并指出了后续优化方向。