目录
[2.1 协议家族(domain)设置](#2.1 协议家族(domain)设置)
[2.2 服务类型(type)设置](#2.2 服务类型(type)设置)
[2.3 协议类型(protocol)设置](#2.3 协议类型(protocol)设置)
[1. 准备地址结构体](#1. 准备地址结构体)
[2. 填充地址信息](#2. 填充地址信息)
[3. 内存清零(将整个结构体清零)](#3. 内存清零(将整个结构体清零))
[4. 执行绑定操作](#4. 执行绑定操作)
[1. INADDR_ANY的使用](#1. INADDR_ANY的使用)
[2. 端口号转换](#2. 端口号转换)
[3. 错误处理](#3. 错误处理)
[4. UDP服务器的绑定](#4. UDP服务器的绑定)
3、监听套接字与连接套接字的分工(超级重要!!!重点记忆!!!)
[1. 监听套接字](#1. 监听套接字)
[2. 已连接套接字](#2. 已连接套接字)
[3. 区别与联系(总结)](#3. 区别与联系(总结))
[4. 联系](#4. 联系)
[5. 工作流程回顾(结合比喻)](#5. 工作流程回顾(结合比喻))
[最可能的原因:TIME_WAIT 状态](#最可能的原因:TIME_WAIT 状态)
[为什么需要 &(取地址)?](#为什么需要 &(取地址)?)
[1. read函数(从套接字读出数据然后写到缓冲区里面)](#1. read函数(从套接字读出数据然后写到缓冲区里面))
[2. write函数(从缓冲区里拿出数据然后写到套接字当中)](#2. write函数(从缓冲区里拿出数据然后写到套接字当中))
[1. 启动你的服务器](#1. 启动你的服务器)
[2. 使用 Telnet 连接测试](#2. 使用 Telnet 连接测试)
[3. 测试回声功能](#3. 测试回声功能)
[4. 测试多行消息](#4. 测试多行消息)
[5. 查看服务器日志](#5. 查看服务器日志)
[6. 断开连接](#6. 断开连接)
[2、为什么 http://公网IP:8081 能访问你的服务?](#2、为什么 http://公网IP:8081 能访问你的服务?)
[通信流程:浏览器 → 公网IP → 云服务商网络 → 你的服务器 → 你的应用(8081端口)](#通信流程:浏览器 → 公网IP → 云服务商网络 → 你的服务器 → 你的应用(8081端口))
一、服务端套接字创建(监听套接字)
在TCP服务器实现中,我们将服务器功能封装为一个类结构。当实例化服务器对象后,首要任务就是进行初始化配置,而创建套接字(Socket)是整个初始化流程中的关键第一步。下面将详细阐述TCP服务器创建套接字的技术细节和实现要点。
1、套接字创建基础
套接字是网络通信的基石,它为应用程序提供了网络通信的端点。在TCP服务器实现中,我们通过调用系统级的socket()函数来创建套接字,该函数的原型通常如下:
cpp
int socket(int domain, int type, int protocol);

2、参数配置详解
2.1 协议家族(domain)设置
cpp
AF_INET
-
选择依据 :我们选择
AF_INET协议家族,因为它专门用于IPv4网络通信 -
技术说明 :该参数指定套接字使用的地址族,
AF_INET表示使用32位IPv4地址格式 -
扩展知识 :对于IPv6网络,应使用
AF_INET6;本地通信可使用AF_UNIX
2.2 服务类型(type)设置
cpp
SOCK_STREAM
-
选择依据:作为TCP服务器,必须选择面向连接的流式套接字
-
特性说明:
-
**有序传输:**保证数据按发送顺序接收
-
**可靠传输:**通过确认机制确保数据完整到达
-
**全双工:**支持双向同时通信
-
**面向连接:**需要建立连接(三次握手)和断开连接(四次挥手)
-
-
对比说明 :与
SOCK_DGRAM(UDP无连接数据报)形成对比
2.3 协议类型(protocol)设置
cpp
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时,表明套接字创建失败,此时应立即终止程序并跳过后续操作。
cpp
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
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地址信息:
cpp
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()将结构体内存清零:
cpp
struct sockaddr_in local;
memset(&local, 0, sizeof(local)); // 或者使用 bzero(&local, sizeof(local));


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

绑定实际上是将文件与网络建立关联。若绑定失败,则无需继续后续操作,直接终止程序即可。
cpp
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服务器绑定的完整实现示例:
cpp
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
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函数,用于将内存区域清零:
cpp
void bzero(void *s, size_t n);
虽然在现代C++中更推荐使用memset(),但bzero()在某些代码库中仍然可见。它的功能与以下memset()调用等价:
cpp
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服务器套接字设置为被动监听状态的关键函数:
cpp
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服务器监听实现示例:
cpp
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#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()函数详解
函数原型
cpp
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转换为点分十进制字符串格式
-
cpp
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
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、服务端测试方法
测试程序
我们可以进行简单测试,验证服务器是否能正常接收请求连接。具体步骤如下:
-
运行服务端程序时需指定端口号
-
使用该端口号创建服务端对象
-
初始化服务端后启动服务
这样就可以完成服务端的部署测试。
cpp
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;
}
测试步骤
-
编译程序:
cppg++ TcpServer.cc -o TcpServer -
启动服务器(例如使用8081端口):
cpp./TcpServer 8081
-
**验证服务器状态:**当服务端启动后,使用netstat命令可以看到一个名为TcpServer的服务进程正在运行。该进程绑定在8081端口,并采用INADDR_ANY地址(显示为0.0.0.0),这意味着服务器能够监听本地所有网卡的数据传输。最关键的是,服务器当前处于LISTEN状态,表示它已准备好接收外部连接请求。
bashnetstat -anp | grep TcpServer应该能看到类似输出:

连接测试方法
尽管尚未编写客户端代码,我们仍可通过telnet命令连接到服务器,因为telnet本质上使用的是TCP协议。如下,使用telnet连接服务器后可以看到,服务器成功接收了一个连接。该连接对应的套接字文件描述符为4。这是因为:
-
0、1、2号文件描述符默认分配给标准输入、输出和错误流
-
3号描述符在服务器初始化时分配给了监听套接字
因此,首个客户端连接请求会获得4号文件描述符的服务(接收)套接字。
方法1:使用telnet测试
一句话概括 :Telnet 是一个用于远程登录到其他计算机的网络协议和命令行工具。它允许你在一台机器上通过命令行控制另一台机器,就像你正坐在那台机器的键盘前一样。
核心功能与用途:
-
远程登录与管理:在个人电脑和服务器上,系统管理员常用它来远程管理服务器、网络设备(如交换机、路由器)等。
-
网络服务调试 :Telnet 不仅可以登录到远程主机,还可以作为一个简单的客户端**(想象成一个不用自己写的万能简单客户端)**,直接连接任何基于 TCP 的服务器端口,用来测试服务是否可用(例如,测试 Web 服务器、邮件服务器)。
基本语法:
bash
telnet [主机名或IP地址] [端口号]
-
主机名或IP地址:你想要连接的目标计算机的地址。
-
端口号:(可选)指定要连接的服务端口。如果不指定,默认使用 23 端口(Telnet 服务默认端口)。
现代替代方案:SSH
正是因为 Telnet 的安全性缺陷,SSH 已经几乎完全取代了它。
-
SSH 提供了与 Telnet 类似的功能(远程命令行登录),但所有通信过程都是加密的,确保了安全和隐私。
-
现在的生产环境和绝大多数系统中,强烈不建议也不应该再开启 Telnet 服务。
现代等效命令:
bash
ssh username@192.168.1.100
总结
-
Telnet 是什么:一个古老的、基于明文的远程登录和网络调试工具。
-
现在还用吗 :基本不再用于实际的远程管理,因为太不安全。
-
现在还怎么用 :主要作为一个简单的网络连通性和服务端口测试工具。例如,快速检查某个服务器的 80 端口或 443 端口是否能连通。
所以,当你今天再看到或使用 telnet 命令时,大概率不是在真正"登录",而是在做网络故障排查。
bash
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结束,又可以绑定了
验证这个理论:(可以手动实操验证一下)
bash
# 第一次运行程序
./TcpServer 8081
# 在另一个终端,快速检查端口状态
sudo netstat -tulpn | grep 8081
# 你会看到类似:tcp 0 0 0.0.0.0:8081 0.0.0.0:* LISTEN <pid>
# 现在Ctrl+C停止程序,然后立即检查
sudo netstat -tulpn | grep 8081
# 你会看到:tcp 0 0 0.0.0.0:8081 0.0.0.0:* TIME_WAIT -
如果想要彻底解决这个问题的话,我们可以在 InitServer() 方法中,在 bind() 调用之前添加以下代码:
cpp
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 状态的端口,彻底解决重启绑定失败的问题。
函数参数详解
cpp
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)
-
-
获取公网访问地址:
bash# 在服务器上查看公网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 功能强大但传输不加密,不适合传输敏感信息。
bash
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函数(从套接字读出数据然后写到缓冲区里面)
cpp
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函数(从缓冲区里拿出数据然后写到套接字当中)
cpp
ssize_t write(int fd, const void *buf, size_t count);

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

4、服务端请求处理实现
当服务端通过read函数接收到客户端数据后,即可调用write函数将这些数据返回给客户端。
处理流程要点
-
双工通信:服务套接字既能读取也能写入数据,体现了TCP的全双工特性
-
资源管理 :处理完成后必须关闭服务套接字,避免文件描述符泄漏**(当从服务套接字读取客户端数据时,若read函数返回值为0或出现读取错误,应立即关闭对应的文件描述符。由于文件描述符本质上是数组索引,系统资源有限,若不及时释放,可用的文件描述符会逐渐耗尽。因此,完成客户端服务后必须及时关闭相关文件描述符,避免造成资源泄漏。)**
-
错误处理:需要处理读取失败和连接关闭的情况
完整代码实现
cpp
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
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] = '\0'; // 确保字符串终止
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命令查看查询网络连接状态,如下,可以看到服务端正处于监听状态:
bash
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. 启动你的服务器
bash
# 编译并运行
g++ TcpServer.cc -o TcpServer
./TcpServer
你会看到输出:

2. 使用 Telnet 连接测试
打开新的终端窗口,执行:
bash
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命令来查看网络连接状态,如下:
bash
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请求:
bashGET / HTTP/1.1 Host: 你的公网IP:8081 User-Agent: Mozilla/5.0... -
只要你的服务能理解HTTP协议并返回有效响应,浏览器就能显示
5、实际测试例子
如果你的服务返回纯文本:
python
# 简单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:浏览器会渲染成漂亮的页面。
python
self.wfile.write(b'<h1>Welcome</h1><p>This is my service</p>')
6、总结
-
浏览器是专门的HTTP客户端,但通过HTTP协议可以访问各种网络服务
-
http://IP:端口的工作原理:浏览器通过TCP连接到指定端口,然后使用HTTP协议通信 -
只要你的服务理解HTTP协议,浏览器就能与之交互
-
浏览器不是真正的"万能客户端",它主要局限于Web相关协议
这就是为什么你可以在浏览器中测试你的后端服务 - 因为浏览器是最方便、最通用的HTTP测试工具!