Linux C++ TCP 服务端经典的监听骨架

一、引言

本文介绍一个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 服务端完整流程

标准答案:

  1. socket() 创建 TCP 套接字;
  2. setsockopt 设置端口复用,解决重启端口占用问题;
  3. bind() 绑定服务器 IP 与端口;
  4. listen() 将套接字转为监听状态,开始监听客户端连接;
  5. accept() 阻塞接受客户端连接,得到客户端通信文件描述符;
  6. recv/send 循环收发数据,实现服务端回声;
  7. 客户端断开或收到退出信号,close() 关闭套接字,优雅退出。

2. 什么是套接字 socket?

标准答案:

socket 是操作系统提供的网络通信接口 ,本质是一个文件描述符 fd,Linux 一切皆文件,网络通信也通过文件描述符读写。服务端监听套接字、客户端连接套接字,都是不同的 fd。

3. 服务端两个 fd 的区别(高频)

代码里有 两个 fdsock(监听 fd)、client_fd(通信 fd)

标准答案:

  1. sock 监听套接字:只负责监听、接受客户端连接,不负责数据收发;
  2. client_fd 连接套接字:专门和已经连接成功的客户端收发数据
  3. 每来一个新客户端,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.backlogTCP 半连接队列长度,存放三次握手未完成的连接数。

5. socket(PF_INET, SOCK_STREAM, 0) 三个参数含义

标准答案:

1.第一个 PF_INET:使用 IPv4 网络协议;

2.第二个 SOCK_STREAM:使用 TCP 流式协议(面向连接、可靠传输);

3.第三个参数 0:默认协议。

6. 为什么要用 htons() 转换端口?

标准答案:

  1. 主机字节序(小端序):电脑 CPU 存储数据格式;
  2. 网络字节序 (大端序):TCP/IP 网络统一传输格式;不同主机大小端不一样,网络必须统一大端。htons:host to network short,主机端口转网络字节序 。同理 htonl 转 IP,ntohs/ntohl 网络转回主机。

7. inet_ptoninet_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, ...);

标准答案:

  1. accept阻塞函数,没有客户端连接时,线程会一直阻塞等待;
  2. listen 的已完成连接队列取出一个连接;
  3. 返回一个新的文件描述符 client_fd,用于后续和客户端通信;
  4. 第一个参数依旧是监听套接字 sock,不是 client_fd。

11. recvsend 返回值含义

标准答案:

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 信号优雅退出有什么意义?

标准答案:

  1. 暴力 kill -9 杀死进程会直接释放资源,容易造成端口残留;
  2. SIGTERM 是温和终止信号,程序可以主动执行逻辑:结束循环、关闭 socket、释放资源,优雅安全退出,避免资源泄漏。
  3. stop 全局标志位用于安全跳出主循环。

15. killkill -9 区别

标准答案:

  • kill pid:发送 SIGTERM 默认信号,程序可以捕获、优雅退出;
  • kill -9 pid:强制杀死进程,无法捕获,内核直接回收资源,服务端不推荐。

16. 客户端和服务端完整 TCP 三次握手过程

结合你代码回答版

  1. 客户端发起 SYN 连接请求;
  2. 服务端响应 SYN+ACK;
  3. 客户端回复 ACK;三次握手完成,连接建立,accept 解除阻塞返回。

17. backlog 参数到底是什么?

标准答案 backlog 代表 已完成连接队列(全连接队列)最大长度 。存放已经完成三次握手、等待 accept 取走的连接。连接超过该数量,新连接会被内核拒绝。

18. TCP 粘包问题(必问,结合代码缺陷)

标准答案:

TCP 是字节流协议,无数据边界,内核会合并多次小数据包一起发送。你当前代码一次收发单行消息暂时没体现,但连续高频发送就会出现粘包,导致读取数据错乱。解决方案:自定义数据包协议(包头 + 包长)、定长包、分隔符。

19. TCP 四次挥手(断开连接过程)

客户端 close 断开时,双方四次挥手断开连接,服务端 recv 返回 0 识别断开。

20. 分析当前阻塞模型服务端缺点

满分标准答案:

  1. 单客户端阻塞模型 ,同一时间只能处理一个客户端连接accept 阻塞、recv 也阻塞,第一个客户端连接后,新客户端无法接入;
  2. 纯阻塞 IO,没有高并发能力,性能极低;
  3. 没有处理网络异常、信号异常容错;
  4. 没有解决 TCP 粘包问题;
  5. 只支持简单字符串收发,无自定义业务协议。

21. 如何让服务端支持多客户端同时连接?

按学习顺序回答(最标准)

  1. 多进程模型 :每一个客户端 fork 子进程处理;
  2. 多线程模型:每一个连接创建一个线程处理;
  3. I/O 多路复用select / poll / epoll,单线程管理大量连接;
  4. Reactor 反应堆模型(大厂游戏服务端主流)。

22. 为什么游戏服务端首选 epoll,而不是多线程?

标准答案:

多线程线程创建销毁开销大、线程切换开销大、线程安全问题多;

epoll 是 Linux 内核实现的 I/O 多路复用,事件驱动,高并发、低开销,百万连接无压力,王者荣耀等手游服务端底层全部基于 epoll。

23. 阻塞 IO 是什么?

acceptrecv 没有事件就一直卡住等待,不返回,占用线程。该代码就是典型阻塞 IO 模型

24. 127.0.0.1 和 0.0.0.0 区别

标准答案

  • 127.0.0.1:本机回环地址,仅本机进程可访问,外部电脑、Windows 无法连接;
  • 0.0.0.0:通配地址,监听本机所有网卡,局域网、外网设备均可连接。

25. send 发送成功就代表客户端一定收到了吗?

标准答案:

不是send 返回仅代表数据拷贝到内核发送缓冲区成功,不代表对方已经接收。TCP 底层滑动窗口、内核缓冲区负责后续传输。

26. 基于这个基础服务端,如何扩展成游戏战斗服务端?

满分回答:

  1. 封装自定义消息协议:消息头(长度、指令号)+ 消息体;
  2. 指令区分:进入房间、移动、技能、心跳、帧同步数据;
  3. 改用 epoll 高并发框架,支持大量玩家同时在线;
  4. 增加房间管理、玩家数据管理、帧同步逻辑;
  5. 增加心跳保活、断线重连、风控校验。

27. 服务端如何检测客户端掉线?

标准答案:

  1. recv 返回值为 0 或负数;
  2. 增加心跳包机制,客户端定时发心跳,服务端超时未收到则判定掉线,回收资源。

八、简答题速记版

  1. TCP 服务端五步:socket → bind → listen → accept → read/write/close
  2. 监听 fd 负责接连接,通信 fd 负责收发数据。
  3. htons 大小端转换,网络统一大端。
  4. SO_REUSEADDR 端口复用,解决重启占用。
  5. recv 返回 0 代表客户端关闭,>0 为字节数,<0 出错。
  6. 阻塞模型缺点:单连接、性能差、无法高并发。
  7. 高并发优化路线:多线程 → select/poll → epoll → Reactor
  8. 优雅退出:捕获 SIGTERM 信号,安全关闭 fd,防止资源泄漏。

九、总结

本文介绍了一个基于Linux的TCP服务端程序实现,主要包括以下内容:

(1)程序功能:绑定IP端口、监听连接、收发消息并回复固定响应,支持优雅退出;

(2)关键实现:使用socket、bind、listen等系统调用,处理信号实现优雅关闭,设置端口复用解决地址占用问题;

(3)运行流程:完整演示了从启动服务到处理客户端连接的数据交互过程;

(4)局限性分析:指出当前单线程阻塞模型的不足,如无法处理多连接、性能低下等问题;

(5)扩展知识:提供了TCP相关面试题的解答,包括套接字原理、网络字节序、高并发优化方案等。

文章为网络编程初学者提供了完整的服务端实现范例,并指出了后续优化方向。

相关推荐
春栀怡铃声2 小时前
【C++修仙录02】筑基篇:类和对象(中)
c++
551只玄猫2 小时前
【计算机网络 实验报告5】IP层协议分析
网络·网络协议·计算机网络·课程设计·ip·实验报告
Zn_lunar2 小时前
autodl tizi+codex cli
运维·服务器·网络
@insist1232 小时前
网络工程师-实战配置篇(一):深入 BGP 与 VRRP,构建高可靠网络
服务器·网络·php·网络工程师·软件水平考试
楼田莉子2 小时前
同步/异步日志系统:日志器管理器模块\全局接口\性能测试
linux·服务器·开发语言·c++·后端·设计模式
故事和你912 小时前
洛谷-数据结构-1-3-集合3
数据结构·c++·算法·leetcode·贪心算法·动态规划·图论
奇妙之二进制2 小时前
zmq源码分析之io_thread_t
linux·服务器
春栀怡铃声2 小时前
【C++修仙录02】筑基篇:类和对象(上)
开发语言·c++·算法
cui_ruicheng2 小时前
Linux IO入门(三):手写一个简易的 mystdio 库
linux·运维·服务器