TCP服务器实现全流程解析(简易回声服务端):从套接字创建到请求处理
目录
一、服务端套接字创建(监听套接字)
1、套接字创建基础
2、参数配置详解
2.1 协议家族(domain)设置
2.2 服务类型(type)设置
2.3 协议类型(protocol)设置
3、TCP服务器套接字创建实现
二、服务端套接字绑定(监听套接字的绑定)
1、套接字绑定的必要性
2、绑定操作的详细步骤
1. 准备地址结构体
2. 填充地址信息
3. 内存清零(将整个结构体清零)
4. 执行绑定操作
3、TCP服务器套接字绑定实现
4、关键点说明
1. INADDR_ANY的使用
2. 端口号转换
3. 错误处理
4. UDP服务器的绑定
5、扩展说明
bzero函数
地址结构体的其他形式
三、TCP服务器监听机制(将监听套接字设定为监听状态)
1、UDP与TCP服务器初始化对比
2、listen函数
参数说明
返回值
3、TCP服务器监听实现
4、关键点说明
5、监听状态的意义(了解,后面会详细讲解)
6、后续步骤
四、TCP服务器获取连接(通过监听套接字获取连接套接字)
1、TCP服务器初始化与连接获取流程
2、accept()函数详解
函数原型
参数说明
返回值
关键特性
3、监听套接字与连接套接字的分工(超级重要!!!重点记忆!!!)
1. 监听套接字
2. 已连接套接字
3. 区别与联系(总结)
4. 联系
5. 工作流程回顾(结合比喻)
4、服务端获取连接的完整实现
5、服务端测试方法
测试程序
测试步骤
连接测试方法
方法1:使用telnet测试
最可能的原因:TIME_WAIT 状态
什么是TIME_WAIT?
函数参数详解
各参数含义
实际效果
为什么需要 &(取地址)?
方法2:使用浏览器测试
方法3:使用nc(netcat)测试
6、关键注意事项
7、常见问题解答
五、TCP服务器请求处理
1、服务端处理请求概述
2、回声服务器实现
3、核心系统调用函数
1. read函数(从套接字读出数据然后写到缓冲区里面)
2. write函数(从缓冲区里拿出数据然后写到套接字当中)
4、服务端请求处理实现
处理流程要点
完整代码实现
5、关键实现细节
6、异常处理与健壮性考虑
7、测试步骤
1. 启动你的服务器
2. 使用 Telnet 连接测试
3. 测试回声功能
4. 测试多行消息
5. 查看服务器日志
6. 断开连接
8、测试要点说明
六、补充扩展:浏览器是什么?
1、浏览器的核心功能
2、为什么 http://公网IP:8081 能访问你的服务?
通信流程:浏览器 → 公网IP → 云服务商网络 → 你的服务器 → 你的应用(8081端口)
3、浏览器是"万能客户端"吗?
浏览器能处理的协议:
浏览器不能直接处理的:
4、关键理解:HTTP协议是基础
5、实际测试例子
如果你的服务返回纯文本:
如果你的服务返回HTML:浏览器会渲染成漂亮的页面。
6、总结
一、服务端套接字创建(监听套接字)
在TCP服务器实现中,我们将服务器功能封装为一个类结构。当实例化服务器对象后,首要任务就是进行初始化配置,而创建套接字(Socket)是整个初始化流程中的关键第一步。下面将详细阐述TCP服务器创建套接字的技术细节和实现要点。
1、套接字创建基础
套接字是网络通信的基石,它为应用程序提供了网络通信的端点。在TCP服务器实现中,我们通过调用系统级的socket()函数来创建套接字,该函数的原型通常如下:
int socket(int domain, int type, int protocol);

2、参数配置详解
2.1 协议家族(domain)设置
AF_INET
-
选择依据:我们选择
AF_INET协议家族,因为它专门用于IPv4网络通信 -
技术说明:该参数指定套接字使用的地址族,
AF_INET表示使用32位IPv4地址格式 -
扩展知识:对于IPv6网络,应使用
AF_INET6;本地通信可使用AF_UNIX
2.2 服务类型(type)设置
SOCK_STREAM
-
选择依据:作为TCP服务器,必须选择面向连接的流式套接字
-
特性说明:
-
有序传输:保证数据按发送顺序接收
-
可靠传输:通过确认机制确保数据完整到达
-
全双工:支持双向同时通信
-
面向连接:需要建立连接(三次握手)和断开连接(四次挥手)
-
-
对比说明:与
SOCK_DGRAM(UDP无连接数据报)形成对比
2.3 协议类型(protocol)设置
0
-
设置原理:当
type参数已明确指定服务类型时,protocol参数可设为0 -
系统行为:操作系统会根据前两个参数自动选择合适的协议(TCP对应IPPROTO_TCP)
-
特殊情况:若需显式指定协议(如使用原始套接字),则需设置具体协议号
3、TCP服务器套接字创建实现
调用socket()函数可创建网络通信端口,成功时返回文件描述符,类似于open()函数:
-
应用程序可通过read/write像操作文件一样进行网络数据传输
-
调用失败时返回-1错误码
-
使用IPv4协议时需将family参数设为AF_INET
-
采用TCP协议时,type参数应设为SOCK_STREAM(面向流的传输协议)
-
protocol参数通常设为0即可(其他情况可省略说明)
套接字创建失败处理优化:当创建套接字返回的文件描述符小于0时,表明套接字创建失败,此时应立即终止程序并跳过后续操作。
#include
#include
#include
#include
#include
#include
class TcpServer
{
public:
void InitServer()
{
//创建套接字
_sock = socket(AF_INET, SOCK_STREAM, 0);
if (_sock < 0){
std::cerr << "socket error" << std::endl;
exit(2);
}
}
~TcpServer()
{
if (_sock >= 0){
close(_sock);
}
}
private:
int _sock; //套接字
};
补充说明:
-
在实际应用中,TCP服务器和UDP服务器创建套接字的流程基本相同,区别仅在于套接字类型的选择:TCP需要指定为流式服务(SOCK_STREAM),而UDP则需指定为用户数据报服务(SOCK_DGRAM)。
-
在服务器析构时,只需关闭对应的文件描述符即可。
二、服务端套接字绑定(监听套接字的绑定)
1、套接字绑定的必要性
在创建完套接字后,我们实际上只是在操作系统层面打开了一个文件描述符,这个套接字还没有与任何具体的网络地址或端口关联。因此,创建套接字后必须调用bind()函数将其绑定到特定的网络地址和端口上,这样才能使服务端能够接收来自客户端的连接请求。

2、绑定操作的详细步骤
1. 准备地址结构体
我们需要定义并填充一个struct sockaddr_in结构体,该结构体用于存储IPv4地址信息:
struct sockaddr_in {
short sin_family; // 地址族,如AF_INET
unsigned short sin_port; // 16位端口号,网络字节序
struct in_addr sin_addr; // 32位IPv4地址,网络字节序
char sin_zero[8]; // 未使用,通常填充为0
};
2. 填充地址信息
在填充地址信息时需要注意以下几点:
-
协议家族:必须设置为
AF_INET表示IPv4协议 -
端口号:需要使用
htons()函数将主机字节序转换为网络字节序 -
IP地址:
-
可以设置为
127.0.0.1表示仅接受本地回环连接 -
可以设置为具体的公网IP地址
-
在云服务器环境中,通常设置为
INADDR_ANY(0.0.0.0)表示本机接受集成在本机中的所有网络接口的连接(从本地(这个本地是指服务器的本地)任何一张网卡当中读取数据)。当服务器拥有多个网卡或单个网卡绑定多个IP时,使用该设置可以让服务器在所有IP地址上监听请求,直到与客户端建立连接时才确定具体使用的IP地址。
-
3. 内存清零(将整个结构体清零)
在填充结构体之前,建议先使用memset()或bzero()将结构体内存清零:
struct sockaddr_in local;
memset(&local, 0, sizeof(local)); // 或者使用 bzero(&local, sizeof(local));


4. 执行绑定操作
调用bind()系统调用将套接字与地址绑定:

绑定实际上是将文件与网络建立关联。若绑定失败,则无需继续后续操作,直接终止程序即可。
if (bind(_sock, (struct sockaddr*)&local, sizeof(local)) < 0) {
std::cerr << "bind error" << std::endl;
exit(3);
}
3、TCP服务器套接字绑定实现
服务器程序通常监听固定的网络地址和端口号,客户端在获知这些信息后即可发起连接请求。服务器需要通过bind()函数绑定指定的地址和端口。
关键点:
-
bind()成功返回0,失败返回-1
-
该函数将socket文件描述符(sockfd)与指定地址(myaddr)绑定,使sockfd能够监听该地址和端口
-
参数myaddr使用通用指针类型struct sockaddr*,可接受不同协议的地址结构
-
由于各协议地址结构长度不同,需通过addrlen参数明确指定结构体长度
在服务器类中需要引入端口号参数,因为TCP服务器初始化时必须指定监听端口。实例化服务器对象时需传入端口号参数。由于使用的是云服务器,绑定IP地址时可直接使用INADDR_ANY而无需指定公网IP,因此服务器类中未包含IP地址参数。以下是TCP服务器绑定的完整实现示例:
#include
#include
#include
#include
#include
#include
class TcpServer {
public:
TcpServer(int port)
: _sock(-1)
, _port(port)
{}
void InitServer() {
// 1. 创建套接字
_sock = socket(AF_INET, SOCK_STREAM, 0);
if (_sock < 0) {
std::cerr << "socket error" << std::endl;
exit(2);
}
// 2. 准备地址结构体并清零
struct sockaddr_in local;
memset(&local, 0, sizeof(local)); // 或者使用 bzero(&local, sizeof(local));
// 3. 填充地址信息
local.sin_family = AF_INET; // IPv4协议
local.sin_port = htons(_port); // 端口号,转换为网络字节序
local.sin_addr.s_addr = INADDR_ANY; // 绑定到所有网络接口
// 4. 执行绑定操作
if (bind(_sock, (struct sockaddr*)&local, sizeof(local)) < 0) {
std::cerr << "bind error" << std::endl;
exit(3);
}
}
~TcpServer() {
if (_sock >= 0) {
close(_sock);
}
}
private:
int _sock; // 监听套接字
int _port; // 服务器端口号
};
4、关键点说明
1. INADDR_ANY的使用
-
在云服务器环境中,通常使用
INADDR_ANY(0.0.0.0)而不是特定IP地址 -
这样服务器会监听所有网络接口上的连接请求
-
不需要进行网络字节序转换,因为0在任何字节序下都是0
2. 端口号转换
-
必须使用
htons()将主机字节序转换为网络字节序 -
这是因为不同CPU架构可能使用不同的字节序(大端/小端)
3. 错误处理
-
绑定失败通常意味着端口已被占用或没有权限
-
在Linux中,1024以下的端口需要root权限
4. UDP服务器的绑定
-
TCP和UDP服务器的绑定过程完全相同
-
区别在于创建套接字时使用的类型(SOCK_STREAM vs SOCK_DGRAM)
5、扩展说明
bzero函数

bzero()是一个传统的BSD函数,用于将内存区域清零:
void bzero(void *s, size_t n);
虽然在现代C++中更推荐使用memset(),但bzero()在某些代码库中仍然可见。它的功能与以下memset()调用等价:
memset(s, 0, n);

地址结构体的其他形式
除了sockaddr_in(用于IPv4),还有:
-
sockaddr_in6:用于IPv6 -
sockaddr:通用地址结构体,用于接受多种类型的地址
在调用bind()等函数时,通常需要将特定类型的地址结构体强制转换为sockaddr*类型。通过以上详细的步骤和说明,我们可以清楚地理解服务端套接字绑定的全过程及其背后的原理。
三、TCP服务器监听机制(将监听套接字设定为监听状态)
1、UDP与TCP服务器初始化对比
UDP服务器和TCP服务器在初始化流程上有显著差异:
UDP服务器:
-
创建套接字
-
绑定地址和端口
-
无需监听,直接进入数据接收/发送状态
TCP服务器:
-
创建套接字
-
绑定地址和端口
-
必须设置为监听状态才能接收客户端连接请求(TCP服务器需要持续监听客户端的连接请求,这要求将服务器创建的套接字设置为监听状态。)
-
然后才能进行后续的连接接受和数据交换
这种差异源于TCP的面向连接特性。TCP需要维护连接状态,而UDP是无连接的。
2、listen函数
listen()函数是将TCP服务器套接字设置为被动监听状态的关键函数:
int listen(int sockfd, int backlog);

listen()函数将sockfd设置为监听状态,最多允许backlog个客户端处于连接等待队列。若超过该数量的连接请求将被自动忽略。通常建议将该值设置为较小的数值(如5)。
参数说明
sockfd:
-
需要设置为监听状态的套接字文件描述符
-
必须是通过
socket()创建的TCP套接字(SOCK_STREAM类型)
backlog:(要重点理解!!!)
-
定义全连接队列(已完成连接队列)的最大长度
-
当多个客户端同时发起连接请求时,未被
accept()处理的连接会暂存在此队列 -
建议值:5-10,过大可能占用过多系统资源
-
注意:现代Linux系统可能使用
/proc/sys/net/core/somaxconn的值作为实际上限
listen() 函数的第二个参数指的是「完整连接队列」的长度,不是两个队列的总和。
1. TCP 连接的两种状态
-
半连接队列(SYN Queue):收到 SYN,但未完成三次握手
-
完整连接队列(Accept Queue):已完成三次握手,等待
accept()取出
2. listen() 的第二个参数
-
backlog:只控制完整连接队列的最大长度 -
不影响半连接队列(半连接队列大小由系统参数控制)
实际工作流程
客户端 SYN ──→ 半连接队列 ──→ 三次握手完成 ──→ 完整连接队列 ──→ accept() 取出
(SYN_RCVD状态) (ESTABLISHED状态)
重要区别
-
半连接队列大小:由
/proc/sys/net/ipv4/tcp_max_syn_backlog控制 -
完整连接队列大小:由
listen()的backlog参数控制
总结:listen() 第二个参数只控制「完整连接队列」的长度!
返回值
-
成功:返回0
-
失败:返回-1,并设置
errno表示具体错误
3、TCP服务器监听实现
TCP服务器在完成套接字创建和绑定后,需将套接字设为监听状态以接收新连接。若监听失败,则服务器无法处理客户端连接请求,此时应立即终止程序。以下是完整的TCP服务器监听实现示例:
#include
#include
#include
#include
#include
#include
#include
#define BACKLOG 5 // 连接队列长度
class TcpServer {
public:
TcpServer(int port) : _port(port), _listen_sock(-1) {}
~TcpServer() {
if (_listen_sock >= 0) {
close(_listen_sock);
}
}
void InitServer() {
// 1. 创建套接字
_listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (_listen_sock < 0) {
std::cerr << "socket creation error: " << strerror(errno) << std::endl;
exit(EXIT_FAILURE);
}
// 2. 设置套接字选项(可选,用于解决地址已在使用中的问题)
int opt = 1;
if (setsockopt(_listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
std::cerr << "setsockopt error: " << strerror(errno) << std::endl;
close(_listen_sock);
exit(EXIT_FAILURE);
}
// 3. 绑定地址和端口
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY; // 监听所有网络接口
if (bind(_listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0) {
std::cerr << "bind error: " << strerror(errno) << std::endl;
close(_listen_sock);
exit(EXIT_FAILURE);
}
// 4. 设置为监听状态
if (listen(_listen_sock, BACKLOG) < 0) {
std::cerr << "listen error: " << strerror(errno) << std::endl;
close(_listen_sock);
exit(EXIT_FAILURE);
}
std::cout << "Server initialized successfully, listening on port " << _port << std::endl;
}
// 其他方法如AcceptConnection()等可以在此处添加
private:
int _listen_sock; // 监听套接字
int _port; // 服务器端口号
};
补充说明:
TCP服务器初始化时创建的套接字并非普通套接字,而是专用的监听套接字。为明确其用途,我们将代码中的变量名从"sock"改为"listen_socket"。
需要注意的是,TCP服务器的初始化必须完成以下三个步骤才算成功:(只有这三个步骤都完成后,TCP服务器才算初始化完成)
-
成功创建套接字
-
成功绑定端口
-
成功开启监听
4、关键点说明
监听套接字:(后面会对比用于监听的套接字和接收的套接字)
-
专门用于监听连接请求的套接字
-
命名使用
_listen_sock比通用sock更清晰表达意图 -
通常一个TCP服务器只需要一个监听套接字
错误处理:每个系统调用后都检查返回值、使用strerror(errno)输出具体错误信息、失败时清理资源并退出程序
资源管理:析构函数中确保套接字被关闭、使用RAII原则管理资源
可选优化:SO_REUSEADDR选项允许快速重启服务器、避免"Address already in use"错误
5、监听状态的意义(了解,后面会详细讲解)
将套接字设置为监听状态后:
-
服务器可以接收客户端的连接请求(SYN包)
-
系统内核会维护两个队列:
-
半连接队列(SYN队列):已收到SYN但未完成三次握手的连接
-
全连接队列(ACCEPT队列):已完成三次握手等待被
accept()的连接
-
-
只有设置为监听状态的套接字才能使用
accept()函数接受新连接
6、后续步骤
完成监听设置后,TCP服务器通常需要:
-
使用
accept()接受新连接 -
为每个连接创建新的套接字进行数据通信
-
可能使用多路复用(select/poll/epoll)管理多个连接
这个监听状态是TCP服务器能够处理多个客户端连接请求的基础机制。
四、TCP服务器获取连接(通过监听套接字获取连接套接字)
1、TCP服务器初始化与连接获取流程
TCP服务器在完成初始化后,需要进入一个持续运行的循环来获取客户端的连接请求。这个过程主要依赖于accept()系统调用,它是TCP服务器实现并发处理的关键函数。
2、accept()函数详解
函数原型
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数说明
-
sockfd:监听套接字的文件描述符,服务器通过这个套接字监听新的连接请求
-
addr:指向
sockaddr结构体的指针,用于返回客户端的地址信息(协议家族、IP地址、端口号等) -
addrlen:输入输出型参数:
-
调用时:传入
addr结构体的长度 -
返回时:实际填充的
addr结构体的长度
-

返回值
-
成功:返回一个新的套接字文件描述符,用于与客户端通信(注意!!!不是监听套接字,而是使用和通过监听套接字来得到一个接收套接字!!!这两者不同!!!)
-
失败:返回-1,并设置errno错误码

关键特性
-
阻塞调用:默认情况下,
accept()会阻塞直到有新的连接到达 -
返回新套接字:每次成功调用都会创建一个新的套接字,专门用于与该客户端通信
3、监听套接字与连接套接字的分工(超级重要!!!重点记忆!!!)
监听套接字:
-
职责:仅用于监听和接受新的连接请求
-
特点:始终保持打开状态,处理完一个连接后可以继续接受新连接
-
生命周期:贯穿整个服务器运行期间
连接套接字(accept返回的套接字):
-
职责:用于与特定客户端进行双向通信
-
特点:每个连接套接字只服务于一个客户端连接
-
生命周期:从
accept()返回开始,到连接关闭结束
我们可以用一个非常贴切的酒店比喻来讲解TCP的监听套接字和已连接套接字的区别与联系。如下:
-
监听套接字:就像酒店门口专门负责迎宾和引路的接待员。
-
已连接套接字:就像酒店里为某张特定餐桌服务的专属服务员。
1. 监听套接字
-
角色:酒店的接待员。
-
工作地点:固定站在酒店大门口。
-
职责:
-
等待任何想进店吃饭的客人(客户端连接请求)。
-
当有客人来时,确认有空位(服务器有能力处理),然后欢迎客人。
-
关键一步:接待员自己不服务客人,而是呼叫一位空闲的服务员过来,把客人交给这位服务员。之后,接待员回到门口,继续等待下一位客人。
-
-
特点:
-
长期存在:只要酒店营业,接待员就在门口。
-
不处理具体事务:不负责点菜、上菜,只负责“建立初次联系”。
-
端口固定:酒店的地址和门牌号是固定的(例如
0.0.0.0:80)。
-
在TCP中:监听套接字通过 socket(), bind(), listen() 系统调用创建。它绑定到一个众所周知的端口(如HTTP服务的80端口),并开始监听连接请求。它自己不用于收发数据。
2. 已连接套接字
-
角色:酒店里的专属服务员。
-
工作地点:在酒店内部,服务于某张特定的餐桌。
-
职责:
-
从接待员手中接过一位特定的客人。
-
负责这位客人的所有具体需求:点菜(接收数据)、上菜(发送数据)、处理问题。
-
与客人建立一一对应的服务关系。
-
-
特点:
-
动态创建:只有在有客人需要服务时才会被创建。
-
处理具体I/O:所有数据的收发都通过它。
-
地址不同:服务员的“位置”是餐桌号(本地IP:端口 + 客户端的IP:端口),这个四元组是唯一的,确保了多个客户端可以同时被正确服务。
-
在TCP中:当监听套接字通过 accept() 接受一个连接请求时,内核会创建一个全新的套接字,这就是已连接套接字。这个新套接字用于与刚刚建立连接的客户端进行通信。
3. 区别与联系(总结)
| 特性 | 监听套接字 | 已连接套接字 |
|---|---|---|
| 比喻 | 酒店门口的接待员 | 酒店内的专属服务员 |
| 创建方式 | socket() -> bind() -> listen() | accept() 返回 |
| 用途 | 等待和接受新的连接 | 与特定客户端进行数据交换 |
| 生命周期 | 长期存在,服务整个运行期间 | 临时存在,连接建立时创建,断开时销毁 |
| 数量 | 通常一个服务端口只有一个 | 可以同时存在很多个,服务多个客户端 |
| 通信对象 | 不直接与任何客户端通信 | 与一个特定的客户端通信 |
| 端口号 | 绑定到一个固定端口(如80) | 使用同一个本地端口,但通过客户端IP:端口来区分 |
4. 联系
-
父子关系:监听套接字是“父”,已连接套接字是“子”。没有监听套接字,就不可能有已连接套接字。
-
分工协作:监听套接字负责“接电话”(建立连接),已连接套接字负责“对话”(传输数据)。这种分工使得服务器能够高效地同时处理多个连接请求。
-
共享端口:所有已连接套接字都共享监听套接字的本地端口号。操作系统通过TCP四元组(源IP、源端口、目标IP、目标端口)来唯一标识一个连接,所以不会混淆。
5. 工作流程回顾(结合比喻)
-
酒店开业:服务器启动,创建监听套接字(接待员就位),绑定到80端口(站在酒店大门口),开始监听。
-
客人A到来:客户端向服务器的80端口发起连接请求(SYN包)。
-
接待员响应:监听套接字
accept()接收到这个请求。它创建一个新的已连接套接字A(呼叫服务员A),并将客人A交给服务员A。 -
接待员归位:监听套接字立刻返回门口,继续等待下一位客人。
-
服务开始:服务员A(已连接套接字A)使用
send()和recv()与客人A进行点菜、上菜等所有数据交互。 -
同时,客人B到来:监听套接字再次
accept(),创建另一个新的已连接套接字B(呼叫服务员B)来服务客人B。 -
此时,酒店有一个接待员(监听套接字)和两个服务员(已连接套接字A和B)在同时工作,互不干扰。
通过这个比喻,我们可以清晰地理解两者在TCP服务器编程中的不同角色和协作方式。
4、服务端获取连接的完整实现
三次握手完成后,服务器调用accept()接收连接:
-
若服务器调用accept()时没有客户端连接请求,将阻塞等待直到有客户端连接;
-
addr是传出参数,accept()返回时会填充客户端的地址和端口信息;
-
若addr参数设为NULL,表示不关心客户端地址信息;
-
addrlen是传入传出参数:
-
传入时指定缓冲区addr的长度,防止溢出;
-
传出时返回客户端地址结构体的实际长度(可能小于缓冲区长度)。
-
服务端获取连接时需注意以下要点:
-
accept函数可能获取连接失败,但TCP服务器不会因此退出。遇到失败时应继续尝试获取新连接。
-
如需输出客户端IP和端口信息,需要:
-
使用inet_ntoa将整数IP转换为字符串格式

-
调用ntohs将端口号从网络字节序转换为主机字节序

-
-
inet_ntoa函数实际上完成了两个转换步骤:
-
将IP地址从网络字节序转换为主机字节序
-
将主机字节序的整数IP转换为点分十进制字符串格式
-
#include
#include
#include
#include
#include
#include
class TcpServer {
public:
TcpServer(int port) : _port(port), _listen_sock(-1) {}
~TcpServer() {
if (_listen_sock >= 0) {
close(_listen_sock);
}
}
void InitServer() {
// 创建套接字
_listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (_listen_sock < 0) {
std::cerr << "socket error" << std::endl;
exit(2);
}
// 绑定地址信息
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
if (bind(_listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0) {
std::cerr << "bind error" << std::endl;
exit(3);
}
// 设置监听
if (listen(_listen_sock, 5) < 0) {
std::cerr << "listen error" << std::endl;
exit(4);
}
}
void Start() {
std::cout << "Server start listening on port " << _port << "..." << std::endl;
while (true) {
// 获取连接
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
socklen_t len = sizeof(peer);
int service_sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
if (service_sock < 0) {
std::cerr << "accept error, continue next" << std::endl;
continue;
}
// 获取客户端信息
std::string client_ip = inet_ntoa(peer.sin_addr);
int client_port = ntohs(peer.sin_port);
std::cout << "Get a new connection -> sock: " << service_sock
<< " [" << client_ip << "]:" << client_port << std::endl;
// 这里可以创建线程或进程来处理这个连接
// HandleConnection(service_sock);
// 简单示例:关闭连接(实际中不会立即关闭)
close(service_sock);
}
}
private:
int _listen_sock; // 监听套接字
int _port; // 服务器端口号
};
5、服务端测试方法
测试程序
我们可以进行简单测试,验证服务器是否能正常接收请求连接。具体步骤如下:
-
运行服务端程序时需指定端口号
-
使用该端口号创建服务端对象
-
初始化服务端后启动服务
这样就可以完成服务端的部署测试。
void Usage(std::string proc) {
std::cout << "Usage: " << proc << " port" << std::endl;
}
int main(int argc, char* argv[]) {
if (argc != 2) {
Usage(argv[0]);
exit(1);
}
int port = atoi(argv[1]);
TcpServer* svr = new TcpServer(port);
svr->InitServer();
svr->Start();
delete svr;
return 0;
}
测试步骤
-
编译程序:
g++ TcpServer.cc -o TcpServer -
启动服务器(例如使用8081端口):
./TcpServer 8081
-
验证服务器状态:当服务端启动后,使用netstat命令可以看到一个名为TcpServer的服务进程正在运行。该进程绑定在8081端口,并采用INADDR_ANY地址(显示为0.0.0.0),这意味着服务器能够监听本地所有网卡的数据传输。最关键的是,服务器当前处于LISTEN状态,表示它已准备好接收外部连接请求。
netstat -anp | grep TcpServer应该能看到类似输出:

连接测试方法
尽管尚未编写客户端代码,我们仍可通过telnet命令连接到服务器,因为telnet本质上使用的是TCP协议。如下,使用telnet连接服务器后可以看到,服务器成功接收了一个连接。该连接对应的套接字文件描述符为4。这是因为:
-
0、1、2号文件描述符默认分配给标准输入、输出和错误流
-
3号描述符在服务器初始化时分配给了监听套接字
因此,首个客户端连接请求会获得4号文件描述符的服务(接收)套接字。
方法1:使用telnet测试
一句话概括:Telnet 是一个用于远程登录到其他计算机的网络协议和命令行工具。它允许你在一台机器上通过命令行控制另一台机器,就像你正坐在那台机器的键盘前一样。
核心功能与用途:
-
远程登录与管理:在个人电脑和服务器上,系统管理员常用它来远程管理服务器、网络设备(如交换机、路由器)等。
-
网络服务调试:Telnet 不仅可以登录到远程主机,还可以作为一个简单的客户端(想象成一个不用自己写的万能简单客户端),直接连接任何基于 TCP 的服务器端口,用来测试服务是否可用(例如,测试 Web 服务器、邮件服务器)。
基本语法:
telnet [主机名或IP地址] [端口号]
-
主机名或IP地址:你想要连接的目标计算机的地址。
-
端口号:(可选)指定要连接的服务端口。如果不指定,默认使用 23 端口(Telnet 服务默认端口)。
现代替代方案:SSH
正是因为 Telnet 的安全性缺陷,SSH 已经几乎完全取代了它。
-
SSH 提供了与 Telnet 类似的功能(远程命令行登录),但所有通信过程都是加密的,确保了安全和隐私。
-
现在的生产环境和绝大多数系统中,强烈不建议也不应该再开启 Telnet 服务。
现代等效命令:
ssh username@192.168.1.100
总结
-
Telnet 是什么:一个古老的、基于明文的远程登录和网络调试工具。
-
现在还用吗:基本不再用于实际的远程管理,因为太不安全。
-
现在还怎么用:主要作为一个简单的网络连通性和服务端口测试工具。例如,快速检查某个服务器的 80 端口或 443 端口是否能连通。
所以,当你今天再看到或使用 telnet 命令时,大概率不是在真正“登录”,而是在做网络故障排查。
telnet 127.0.0.1 8081

此时我们返回服务器,可以看到会显示新连接信息,包括套接字描述符和客户端信息,如下:

我们还可以开多个终端窗口同时连接,观察每个连接分配的不同描述符。如下:
这时我们如果再通过其他窗口再次使用telnet命令向该TCP服务器发起连接请求时,会得到如下结果,这是因为在服务端代码中的Start函数最后的close(service_sock);,它在接收到了客户端的请求之后,服务端成功获取连接,处理完业务后直接关闭了连接套接字,所以分配的还是4号文件描述符(但是对应的客户端的端口号不同),除非删除close函数那一行代码:

删除后,我们这时如果再运行服务端,会发现服务端的监听套接字绑定失败!!!

然而,我们使用下面的命令查看端口号8081是否被占用时,却没有对应的输出,意思是8081这个端口号没有被占用!!!

最可能的原因:TIME_WAIT 状态
什么是TIME_WAIT?
当TCP连接关闭时,主动关闭的一方(服务器)会进入TIME_WAIT状态,通常持续60秒(2MSL)。在这期间,端口不能被立即重用。
场景重现:
-
第一次运行:程序启动,绑定8081成功
-
第一次退出:程序关闭,监听套接字关闭,进入TIME_WAIT
-
立即第二次运行:尝试绑定8081,但端口还在TIME_WAIT中,所以失败
-
等待一段时间后:TIME_WAIT结束,又可以绑定了
验证这个理论:(可以手动实操验证一下)
# 第一次运行程序
./TcpServer 8081
# 在另一个终端,快速检查端口状态
sudo netstat -tulpn | grep 8081
# 你会看到类似:tcp 0 0 0.0.0.0:8081 0.0.0.0:* LISTEN
# 现在Ctrl+C停止程序,然后立即检查
sudo netstat -tulpn | grep 8081
# 你会看到:tcp 0 0 0.0.0.0:8081 0.0.0.0:* TIME_WAIT -
我们除了可以等待TIME_WAIT结束这个方法,我们还可以更换端口号重新进行操作,但是这只是临时可行。如果想要彻底解决这个问题的话,我们可以在 InitServer() 方法中,在 bind() 调用之前添加以下代码:
void InitServer() {
// 创建套接字
_listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (_listen_sock < 0) {
std::cerr << "socket error" << std::endl;
exit(2);
}
// === 添加这几行代码解决TIME_WAIT问题 ===
int reuse = 1;
if (setsockopt(_listen_sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0) {
std::cerr << "setsockopt error" << std::endl;
// 这里不退出,继续尝试绑定
}
// ====================================
// 绑定地址信息
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
if (bind(_listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0) {
std::cerr << "bind error" << std::endl;
exit(3);
}
// 其余代码不变...
}
添加位置:在 socket() 创建之后,bind() 绑定之前。
作用:这几行代码设置 SO_REUSEADDR 选项,允许立即重新绑定处于 TIME_WAIT 状态的端口,彻底解决重启绑定失败的问题。
函数参数详解
setsockopt(_listen_sock, // 要设置哪个套接字(哪栋大楼)
SOL_SOCKET, // 要设置套接字本身的选项(大楼结构)
SO_REUSEADDR, // 具体选项:地址重用(快速重新开业)
&reuse, // 设置的值:1表示开启(&取地址)
sizeof(reuse)); // 值的大小
各参数含义
-
_listen_sock:你创建的监听套接字,就像酒店大楼 -
SOL_SOCKET:表示要设置"套接字层面"的选项,就像设置大楼的基础属性 -
SO_REUSEADDR:具体的选项名称,意思是"允许地址重用" -
&reuse:设置的值(1=开启,0=关闭),&表示取地址 -
sizeof(reuse):告诉系统这个值有多大(int类型通常是4字节)
实际效果
没有设置 SO_REUSEADDR:酒店关门 → 需要打扫60分钟(TIME_WAIT)→ 才能重新开业
设置了 SO_REUSEADDR:酒店关门 → 立即可以重新开业(跳过打扫等待)
为什么需要 &(取地址)?
因为 setsockopt 函数需要知道值在内存中的位置,就像:
-
不说"数字1",而是说"1号保险箱里的东西"
-
这样函数就能去那个位置读取数据
解决好了上面的问题之后,我们可以通过其他窗口再次使用telnet命令向该TCP服务器发起连接请求时,系统会为该客户端分配一个新的套接字,其对应的文件描述符为5:

方法2:使用浏览器测试
也可以直接通过浏览器访问这个TCP服务器。由于浏览器默认使用HTTP/HTTPS协议,而这些协议底层都基于TCP,因此浏览器同样能与该TCP服务器建立连接。但是我们想一下之前遇到的问题,就是:
-
云服务器的公网IP是虚拟的(NAT后面)
-
内网IP在公网不可用
-
想在浏览器中测试服务,但无法直接访问
我们可以使用云服务商的安全组/防火墙规则来解决这个问题:
-
配置安全组:
-
登录云服务商控制台(阿里云、腾讯云等)
-
找到你的云服务器实例
-
配置安全组,放行8081端口
-
通常需要添加规则:协议TCP,端口8081,源IP 0.0.0.0/0(或你的本地公网IP)
-
-
获取公网访问地址:
# 在服务器上查看公网IP curl ifconfig.me # 或者 curl ip.sb
-
在浏览器访问:http://你的公网IP:8081

然后在浏览器地址栏输入:http://你的公网IP:8081

-
浏览器会尝试建立TCP连接
-
服务器会记录连接信息
-
注意:浏览器可能会建立多个连接(如主连接和资源连接)

关于浏览器为何会向TCP服务器发送三次请求的问题,我们暂不深入探讨(后面会详细讲解!!!)。本文的重点在于验证TCP服务器能够正常接收并处理外部请求连接。
方法3:使用nc(netcat)测试
Netcat 是网络工具中的"瑞士军刀",它可以用简单的命令完成各种网络读写操作,比如创建 TCP/UDP 连接、端口扫描、文件传输等。nc 是比 telnet 更好的端口测试工具,因为它更简单直接!
nc 的主要用途:
-
端口检测:快速检查端口是否开放
-
网络调试:手动测试网络服务
-
简单传输:临时文件传输或消息通信
-
网络工具:替代telnet进行基本的网络测试
注意: nc 功能强大但传输不加密,不适合传输敏感信息。
nc 113.45.79.2 8081

上面的例子要开放端口号8081才行,否则如下:

6、关键注意事项
错误处理:accept()可能失败(如被信号中断),服务器应继续运行、需要检查返回值并处理错误情况
地址转换:inet_ntoa():将网络字节序的IP地址转换为点分十进制字符串、ntohs():将网络字节序的端口号转换为主机字节序
资源管理:每个连接套接字在使用后应正确关闭、监听套接字应在服务器退出时关闭
并发处理:示例中简单关闭了连接,实际服务器应为每个连接创建处理线程或进程、可以使用多线程、多进程或I/O多路复用(select/poll/epoll)处理并发
7、常见问题解答
Q: 为什么浏览器访问会建立多个连接?
A: 现代浏览器通常会:
-
建立主连接获取HTML
-
解析HTML后建立额外连接获取CSS、JS等资源
-
可能使用HTTP/1.1的连接复用或HTTP/2的多路复用
Q: 文件描述符分配规律是什么?
A: 在Linux系统中:
-
0: 标准输入
-
1: 标准输出
-
2: 标准错误
-
3: 通常分配给监听套接字
-
后续连接套接字从4开始递增分配
Q: 如何验证服务器确实在接收连接?
A: 除了观察程序输出,还可以:使用netstat或ss命令查看连接状态、使用lsof命令查看打开的套接字、使用网络抓包工具(如Wireshark)观察TCP握手过程
通过以上详细的实现和测试方法,可以全面理解TCP服务器如何获取客户端连接,并为后续实现完整的网络通信功能打下基础。
五、TCP服务器请求处理
1、服务端处理请求概述
在TCP服务器成功获取连接请求后,接下来需要处理客户端连接。这里需要明确的是,监听套接字(listen_sock)仅用于接受新连接,而实际为客户端提供服务的任务则由accept()函数返回的"服务(接收)套接字"承担。这种设计使得监听套接字能够继续监听新的连接请求。
2、回声服务器实现
为了验证通信正常,我们将实现一个简单的回声TCP服务器。该服务器会:
-
接收客户端发送的数据
-
将接收到的数据输出到服务端控制台
-
将相同数据原样发回客户端
-
客户端收到响应后打印输出
这种设计可以确保服务端和客户端之间的双向通信正常工作。
3、核心系统调用函数
1. read函数(从套接字读出数据然后写到缓冲区里面)
ssize_t read(int fd, void *buf, size_t count);

参数说明:
-
fd:文件描述符,指定从哪个套接字读取数据 -
buf:缓冲区指针,用于存储读取到的数据 -
count:期望读取的最大字节数
返回值说明:
-
>0:实际读取的字节数 -
=0:对端已关闭连接 -
<0:读取过程中发生错误

特殊情况处理:
当read()返回0时,表示客户端已关闭连接,这与本地进程间通信(如管道)的行为类似,类似管道通信中写端关闭后读端会读到0的情况,服务端此时应关闭对应的服务套接字:
-
当写端进程停止写入而读端进程持续读取时,读端进程会被挂起,因为此时没有数据可供读取。
-
反之,若读端进程停止读取而写端进程持续写入,当管道写满后,写端进程会被挂起,因为此时没有可用空间。
-
当写端进程完成数据写入并关闭写端后,读端进程在读取完管道中剩余数据后会读到0值。
-
若读端进程关闭读端,写端进程会被操作系统终止,因为其写入的数据已无法被读取。
在客户端-服务端模型中,写端对应客户端。当客户端关闭连接后,服务端读取完套接字中的信息会收到0值。此时若服务端read函数返回0,即可终止对该客户端的服务。
2. write函数(从缓冲区里拿出数据然后写到套接字当中)
ssize_t write(int fd, const void *buf, size_t count);

参数说明:
-
fd:文件描述符,指定向哪个套接字写入数据 -
buf:要发送的数据缓冲区 -
count:要发送的字节数
返回值说明:
-
>0:实际写入的字节数 -
=-1:写入失败,可通过errno获取错误原因

4、服务端请求处理实现
当服务端通过read函数接收到客户端数据后,即可调用write函数将这些数据返回给客户端。
处理流程要点
-
双工通信:服务套接字既能读取也能写入数据,体现了TCP的全双工特性
-
资源管理:处理完成后必须关闭服务套接字,避免文件描述符泄漏(当从服务套接字读取客户端数据时,若read函数返回值为0或出现读取错误,应立即关闭对应的文件描述符。由于文件描述符本质上是数组索引,系统资源有限,若不及时释放,可用的文件描述符会逐渐耗尽。因此,完成客户端服务后必须及时关闭相关文件描述符,避免造成资源泄漏。)
-
错误处理:需要处理读取失败和连接关闭的情况
完整代码实现
#include
#include
#include
#include
#include
#include
#include
class TcpServer {
public:
TcpServer(int port) : _port(port) {
// 初始化监听套接字
_listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (_listen_sock < 0) {
std::cerr << "socket create error" << std::endl;
exit(1);
}
// 设置SO_REUSEADDR选项
int opt = 1;
setsockopt(_listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 绑定端口
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
if (bind(_listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0) {
std::cerr << "bind error" << std::endl;
exit(2);
}
// 开始监听
if (listen(_listen_sock, 5) < 0) {
std::cerr << "listen error" << std::endl;
exit(3);
}
}
~TcpServer() {
if (_listen_sock >= 0) {
close(_listen_sock);
}
}
void Service(int sock, const std::string& client_ip, int client_port) {
char buffer[1024];
while (true) {
// 读取客户端数据
ssize_t size = read(sock, buffer, sizeof(buffer) - 1);
if (size > 0) {
buffer[size] = ' '; // 确保字符串终止
std::cout << "[" << client_ip << ":" << client_port << "] say: " << buffer << std::endl;
// 回声数据回客户端
ssize_t write_size = write(sock, buffer, size);
if (write_size < 0) {
std::cerr << "[" << client_ip << ":" << client_port << "] write error" << std::endl;
break;
}
}
else if (size == 0) {
// 客户端关闭连接
std::cout << "[" << client_ip << ":" << client_port << "] close connection" << std::endl;
break;
}
else {
// 读取错误
std::cerr << "[" << client_ip << ":" << client_port << "] read error" << std::endl;
break;
}
}
// 关闭服务套接字
close(sock);
std::cout << "[" << client_ip << ":" << client_port << "] service completed" << std::endl;
}
void Start() {
std::cout << "Server start on port: " << _port << std::endl;
while (true) {
// 接受新连接
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
if (sock < 0) {
std::cerr << "accept error, continue..." << std::endl;
continue;
}
// 获取客户端信息
std::string client_ip = inet_ntoa(peer.sin_addr);
int client_port = ntohs(peer.sin_port);
std::cout << "New connection from [" << client_ip << ":" << client_port << "], sock: " << sock << std::endl;
// 处理客户端请求
Service(sock, client_ip, client_port);
}
}
private:
int _listen_sock; // 监听套接字
int _port; // 服务器端口
};
int main() {
TcpServer server(8081); // 创建服务器实例,监听8081端口
server.Start(); // 启动服务器
return 0;
}
我们编译运行服务端,然后使用netstat命令查看查询网络连接状态,如下,可以看到服务端正处于监听状态:
netstat -tunlp
-
-t:TCP 连接 -
-u:UDP 连接 -
-n:显示数字地址(不解析主机名) -
-l:仅显示监听中的连接 -
-p:显示进程信息

5、关键实现细节
初始化阶段:创建监听套接字、设置SO_REUSEADDR选项避免"Address already in use"错误、绑定到指定端口、开始监听连接
服务循环:使用accept()接受新连接、获取客户端IP和端口信息、为每个连接创建独立的服务套接字
请求处理:
-
使用
read()接收客户端数据 -
处理三种情况:
-
成功读取数据:输出并回声
-
读取到0:客户端关闭连接
-
读取错误:记录错误并关闭连接
-
-
使用
write()发送响应数据 -
最终关闭服务套接字释放资源
资源管理:每次服务完成后关闭服务套接字、析构函数中确保监听套接字被关闭
6、异常处理与健壮性考虑
-
错误处理:系统调用失败时的错误检测、网络中断等异常情况的处理
-
资源泄漏防护:确保所有打开的文件描述符最终都会被关闭、使用RAII模式管理资源
-
性能考虑:缓冲区大小的选择(1024字节)、字符串终止符的处理
这个实现提供了一个健壮的TCP回声服务器框架,可以作为更复杂网络应用的基础。
7、测试步骤
虽然现在我们并没有写客户端,但是通过上面的telnet命令可以完全当做客户端来进行连接测试!!!如下:
1. 启动你的服务器
# 编译并运行
g++ TcpServer.cc -o TcpServer
./TcpServer
你会看到输出:

2. 使用 Telnet 连接测试
打开新的终端窗口,执行:
telnet 127.0.0.1 8081
如果连接成功,你会看到:(光标在闪烁,等待输入)

3. 测试回声功能
现在你可以输入任何文本,服务器都会原样返回:(如果在telnet中输错了的话,我们可以按住Ctrl,然后再使用Backspace来进行删除刚刚输入的错误)
在telnet中输入:hello server!!! 你应该看到:hello server!!!

再测试:this is a test message 输出:this is a test message

4. 测试多行消息

每输入一行,服务器都会立即回声返回。
5. 查看服务器日志
在服务器终端,你会看到类似这样的输出:

此时我们再开一个新的终端窗口使用netstat命令来查看网络连接状态,如下:
netstat -tunp | grep :8081
使用上面这个命令,而不是之前的那个命令来查看的原因:
-
-l参数:只显示监听状态的端口 -
服务器:在8081端口监听,所以会被
netstat -tunlp显示 -
telnet连接:是已建立的连接,不是监听端口,所以不会显示

通过上面的演示结果,我们可以知道这显示了一个完整的TCP连接对:
输出结果的每一列属性:协议、接收队列、发送队列、本地地址:端口、远程地址:端口、状态、进程
第一条:Telnet客户端
-
本地地址:
127.0.0.1:38680(客户端使用随机端口38680) -
远程地址:
127.0.0.1:8081(连接到服务器8081端口(也就是TcpServer的端口)) -
进程:
12586/telnet- Telnet客户端进程 -
状态:
ESTABLISHED- 连接已建立
第二条:TCP服务器
-
本地地址:
127.0.0.1:8081(服务器监听8081端口(也就是TcpServer的端口)) -
远程地址:
127.0.0.1:38680(连接到Telnet客户端) -
进程:
12295/./TcpServer- C++服务器程序 -
状态:
ESTABLISHED- 连接已建立
关键信息
-
连接正常:双方都是ESTABLISHED状态
-
通信畅通:接收队列和发送队列都是0,说明数据正常传输
-
本地回环:使用127.0.0.1,说明是本地测试
-
端口使用:服务器固定端口:8081、客户端随机端口:38680
这是一个完美的TCP连接,说明:TcpServer程序运行正常、Telnet客户端成功连接、双方正在正常通信。现在我们可以在telnet窗口中测试发送消息,服务器会回声返回!
6. 断开连接
在telnet中按 Ctrl + ],然后输入 quit:

服务器会显示:
因为当写端进程停止写入而读端进程持续读取时,读端进程会被挂起,因为此时没有数据可供读取。read会被阻塞,在telnet客户端退出后,此时当写端进程完成数据写入并关闭写端后,读端进程在读取完管道中剩余数据后会读到0值。然后输出第一行,最后输出第二行结果。

8、测试要点说明
-
实时交互:telnet是双向通信,输入立即得到响应
-
文本协议:服务器处理的是纯文本数据
-
连接管理:可以测试正常断开和异常断开
-
并发测试:可以开多个telnet窗口同时连接
六、补充扩展:浏览器是什么?
思考:通过上面的例子我们可以思考这样的一个问题——为什么浏览器能够使用http://你的服务器公网IP:8081这样的方式来访问我的服务端呢?浏览器究竟是什么?它是一个万能的客户端?是这样理解吗?
实际上,浏览器本质上是一个专门用于处理HTTP/HTTPS协议的客户端程序,但它确实可以看作是一个"多功能网络客户端"。
1、浏览器的核心功能
-
解析URL:理解
http://IP:端口/路径这样的地址 -
建立TCP连接:与目标服务器建立网络连接
-
发送HTTP请求:按照HTTP协议格式发送请求
-
渲染响应:将服务器返回的HTML/CSS/JS内容渲染成可视化页面
2、为什么 http://公网IP:8081 能访问你的服务?
通信流程:浏览器 → 公网IP → 云服务商网络 → 你的服务器 → 你的应用(8081端口)
-
URL解析:浏览器解析
http://你的公网IP:8081 -
DNS查询:如果是域名,会先解析为IP(这里是直接IP,跳过这步)
-
TCP连接:浏览器向
你的公网IP:8081发起TCP连接请求 -
路由转发:云服务商的网络设备将公网IP映射到你的内网服务器
-
服务响应:你的服务器上的应用在8081端口接收请求并响应
3、浏览器是"万能客户端"吗?
上面的提问的思考问题理解部分正确,但有局限性:
浏览器能处理的协议:
-
HTTP/HTTPS:主要功能
-
WebSocket:实时通信
-
FTP:文件传输(部分支持)
-
mailto:邮件链接
-
file:本地文件
浏览器不能直接处理的:
-
原始TCP连接(除了WebSocket)
-
UDP协议
-
SSH/Telnet等专用协议
-
自定义二进制协议
4、关键理解:HTTP协议是基础
当你在浏览器输入 http://IP:8081 时:
-
浏览器默认使用HTTP协议
-
它向指定IP和端口发送HTTP请求:
GET / HTTP/1.1 Host: 你的公网IP:8081 User-Agent: Mozilla/5.0... -
只要你的服务能理解HTTP协议并返回有效响应,浏览器就能显示
5、实际测试例子
如果你的服务返回纯文本:
# 简单Python HTTP服务
from http.server import BaseHTTPRequestHandler, HTTPServer
class SimpleHandler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header('Content-type', 'text/plain')
self.end_headers()
self.wfile.write(b'Hello from my server!')
HTTPServer(('0.0.0.0', 8081), SimpleHandler).serve_forever()
访问 http://公网IP:8081,浏览器会显示:Hello from my server!
如果你的服务返回HTML:浏览器会渲染成漂亮的页面。
self.wfile.write(b'Welcome
This is my service
')
6、总结
-
浏览器是专门的HTTP客户端,但通过HTTP协议可以访问各种网络服务
-
http://IP:端口的工作原理:浏览器通过TCP连接到指定端口,然后使用HTTP协议通信 -
只要你的服务理解HTTP协议,浏览器就能与之交互
-
浏览器不是真正的"万能客户端",它主要局限于Web相关协议
这就是为什么你可以在浏览器中测试你的后端服务 - 因为浏览器是最方便、最通用的HTTP测试工具!









