欢迎来到我的频道!【点击跳转专栏】
本文所有代码已托管至码云:【点此转跳】
文章目录
- [1. 接口初识](#1. 接口初识)
- [1.1 socket()](#1.1 socket())
- [1.2 sockaddr_in](#1.2 sockaddr_in)
- [补充:bzero(清零) && inet_addr(字符串转4字节数)](#补充:bzero(清零) && inet_addr(字符串转4字节数))
- 补充:4字节数转字符串,字符串转4字节
- [补充:hton* 和 ntoh*](#补充:hton* 和 ntoh*)
- [1.3 bind()](#1.3 bind())
- [1.4 recvfrom()](#1.4 recvfrom())
- [1.5 sendto()](#1.5 sendto())
- [1.6 inet_ntoa()](#1.6 inet_ntoa())
- [2. V1版本 - Echo server](#2. V1版本 - Echo server)
- [3. V2版本- DictServer](#3. V2版本- DictServer)
- [4. V3版本- 简单聊天室](#4. V3版本- 简单聊天室)
- [4.1 核心思路与流程图](#4.1 核心思路与流程图)
- [4.2 服务端](#4.2 服务端)
- [1. 服务端创建 UDP Socket](#1. 服务端创建 UDP Socket)
- [2. 接收客户端消息](#2. 接收客户端消息)
- [3. 维护在线用户列表](#3. 维护在线用户列表)
- [4. 为什么要使用线程池](#4. 为什么要使用线程池)
- [5. route_message() 的职责](#5. route_message() 的职责)
- [4.3 客户端](#4.3 客户端)
- [1. 发送线程](#1. 发送线程)
- [2. 接收线程](#2. 接收线程)
- [5 .一次完整聊天流程](#5 .一次完整聊天流程)
- [5. 一些补充内容](#5. 一些补充内容)
- [5.1 netstat指令](#5.1 netstat指令)
- [5.2 ssh与sshd是什么](#5.2 ssh与sshd是什么)
- [5.3 在其他系统进行套接字编程访问云服务器](#5.3 在其他系统进行套接字编程访问云服务器)
1. 接口初识
1.1 socket()
socket() 用来创建一个 Socket,也就是创建一个"通信端点"。它成功后返回一个文件描述符,后续用 bind()、connect()、send()、recv() 等函数操作它。
c
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
三个参数分别表示:
| 参数 | 含义 | 常见取值 |
|---|---|---|
domain |
使用哪类地址 | AF_INET:IPv4;AF_INET6:IPv6;AF_UNIX:本机进程通信 |
type |
通信方式 | SOCK_STREAM:可靠字节流;SOCK_DGRAM:数据报;SOCK_RAW:原始报文 |
protocol |
具体协议 | 通常填 0,系统根据前两个参数自动选择 ,在网络中基本上指明type就自动知道是哪个协议了 |
最常见的 TCP Socket:
c
int fd = socket(AF_INET, SOCK_STREAM, 0);
含义是:创建一个 IPv4、面向连接、可靠传输的 Socket,系统自动选择 TCP。
UDP Socket:
c
int fd = socket(AF_INET, SOCK_DGRAM, 0);
本机 Unix 域 Socket:
c
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
返回值:
- 成功:返回非负整数
fd - 失败:返回
-1,错误原因放在errno
例如:
c
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1) {
perror("socket");
}
注意:
socket()只是"创建电话",还没有拨号。
- 服务端通常继续:
bind()→listen()→accept()- 客户端通常继续:
connect()- 用完后:
close(fd)
1.2 sockaddr_in
struct sockaddr_in 是 IPv4 Socket 地址结构体,用来保存"IP 地址 + 端口号"。
c
#include <netinet/in.h>
struct sockaddr_in
{
sa_family_t sin_family; // 地址族
in_port_t sin_port; // 端口号
struct in_addr sin_addr; // IPv4 地址
unsigned char sin_zero[8]; // 填充字段
};
-----------------
-------------------
各字段含义:
sin_family(本质为unsigned short int):通常填AF_INET,表示使用 IPv4。sin_port:端口号,必须用网络字节序保存,所以使用htons()。sin_addr:IP 地址,通常用inet_pton()转换。sin_zero:没有业务含义,初始化为0即可。
补充:bzero(清零) && inet_addr(字符串转4字节数)
bzero():
作用:把一块内存全部清零。
c
#include <strings.h>
void bzero(void *s, size_t n);
例如初始化地址结构体:
c
struct sockaddr_in addr;
bzero(&addr, sizeof(addr));
这样 addr 的端口、IP、填充字段都会先变成 0,避免未初始化的数据造成问题。
不过 bzero() 是旧接口,现在更推荐:
c
memset(&addr, 0, sizeof(addr));
或者直接:
c
struct sockaddr_in addr = {0};
inet_addr()
作用:把点分十进制 IPv4 字符串转换为网络字节序的 32 位地址(自动转大端)。
c
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);
例如:
c
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
它把 "127.0.0.1" 转成 Socket 能使用的二进制 IP 地址。
补充:4字节数转字符串,字符串转4字节
可以这么写
cpp
struct ip
{
uint32_t parta:8;
uint32_t partb:8;
uint32_t partc:8;
uint32_t partd:8;
};
核心知识点:位域(bit‑field)
语法格式:
类型 成员名 : 占用多少位;
parta:8 代表:这个成员只占用 8 bit
整体结构解析:
底层容器是 uint32_t(32位无符号整型,一共32bit)
parta 占8bit + partb占8bit + partc占8bit + partd占8bit = 32 bit
内存排布:
bit 31~24 │ bit23~16 │ bit15~8 │ bit7~0
parta partb partc partd
注:位域高低位顺序依赖编译器大小端,上图是小端的形式。
使用示例1 ,4字节转字符串
c
// 1.定义一个32位无符号整数存放IP数值
uint32_t ip_addr = XXXX;
// 2.强制类型转换:把ip_addr的内存地址,强行当成 struct ip* 位域结构体指针
struct ip *p = (struct ip *)&ip_addr;
//3.取出四个位域成员,拼接成 "a.b.c.d" 格式的IP字符串
to_string(p->parta)+"." + to_string(p->partb)+"."+to_string(p->partc)+"."+to_string(p->partd)
使用示例2 , 字符串转4字节
将 IP 字符串 192.168.1.12 拆分出四段数值 192、168、1、12,借助指针强制转换位域结构体,写入到 32 位整型变量 uint32_t ip_addr
cpp
((struct ip*)&ip_addr)->parta = 192;
((struct ip*)&ip_addr)->partb = 168;
((struct ip*)&ip_addr)->partc = 1;
((struct ip*)&ip_addr)->partd = 12;
补充:hton* 和 ntoh*
把主机字节序的整数,转换成网络字节序,或者反过来
c
#include <arpa/inet.h>
uint16_t htons(uint16_t hostshort);
网络字节序规定使用大端序。但多数电脑是小端序,所以端口号等数据发送到网络前要转换,如:
c
addr.sin_port = htons(8080);
如果不转换,在小端机器上 8080 可能会被对方当成另一个端口号。
常见配套函数:
| 函数 | 含义 |
|---|---|
htons() |
主机 16 位 → 网络字节序 |
htonl() |
主机 32 位 → 网络字节序 |
ntohs() |
网络 16 位 → 主机字节序 |
ntohl() |
网络 32 位 → 主机字节序 |
1.3 bind()
bind() 就是给 Socket 绑定一个本地"地址"。
对 IPv4 来说,这个地址是:
text
本机 IP + 端口号
例如:
text
0.0.0.0:8080
表示程序占用本机的 8080 端口,等待别人连接。
c
int bind(int sockfd,
const struct sockaddr *addr,
socklen_t addrlen);
sockfd:socket()创建出来的 Socket。addr:要绑定的地址。addrlen:地址结构大小。
1.4 recvfrom()
recvfrom() 用来从 Socket 接收数据,并且可以得到发送方的地址。它最常用于 UDP。
c
#include <sys/socket.h>
ssize_t recvfrom(int sockfd,
void *buf,
size_t len,
int flags,
struct sockaddr *src_addr,
socklen_t *addrlen);
各参数:
sockfd:Socket 文件描述符。buf:接收数据的缓冲区。len:缓冲区大小。flags:通常填0,表示阻塞读取src_addr:接收方地址结构体,函数会写入"谁发来的"。addrlen:传入src_addr的大小,返回实际地址长度前面的参数
buf len表示接受的信息本身src_addr和addrlen是用来标识信息是谁发送的!本质是是个输出型参数
UDP 示例:
c
char buf[1024];
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
ssize_t n = recvfrom(fd, buf, sizeof(buf) - 1, 0,
(struct sockaddr *)&client_addr,
&client_len);
if (n >= 0)
{
buf[n] = '\0';
printf("收到:%s\n", buf);
}
- 客户端调用
recvfrom() :地址参数通常得到 服务端地址 ; - 服务端调用
recvfrom() :地址参数通常得到 客户端地址 ,服务端依靠这个地址把数据回发给客户端。
返回值:
- 大于等于
0:实际收到的字节数。-1:出错,检查errno。
注意:
- UDP 会保留一条报文的边界;如果
buf太小,超出的部分通常会被丢弃。- 默认是阻塞调用,没有数据时会等待。
1.5 sendto()
sendto() 用来通过 Socket 向指定的 IP 和端口发送数据,最常用于 UDP。
c
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
参数含义:
sockfd:之前用socket()创建的套接字。buf:要发送的数据地址。len:数据长度。flags:通常填0,默认为阻塞发送。dest_addr:目标地址,通常是sockaddr_in。addrlen:目标地址结构大小,IPv4 通常填sizeof(struct sockaddr_in)。
UDP发送方式一般是默认全双工
UDP 发送示例:
c
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = inet_addr("192.168.1.100");
char message[] = "hello";
int result = sendto(sockfd, message, strlen(message), 0,
(struct sockaddr *)&server_addr,
sizeof(server_addr));
if (result == -1) {
perror("sendto failed");
}
返回值:
- 成功:返回已交给系统发送的字节数。
- 失败:返回
-1,可用perror()查看原因。
注意:sendto() 成功只表示数据已交给本机系统处理,UDP 不保证对方一定收到。每次调用通常对应一条独立的 UDP 报文。
它和 recvfrom() 是一对:sendto() 发送数据,recvfrom() 接收数据并可得到发送方地址。
1.6 inet_ntoa()
inet_ntoa() 用来把网络字节序的 IPv4 地址转换成点分十进制字符串。
c
#include <arpa/inet.h>
char *inet_ntoa(struct in_addr in);
例如:
c
struct in_addr address;
address.s_addr = htonl(0xC0A80101);
printf("%s\n", inet_ntoa(address));
// 192.168.1.1
简单记忆:
text
inet_addr() :字符串 IP -> 网络字节序 IP
inet_ntoa() :网络字节序 IP -> 字符串 IP
2. V1版本 - Echo server
代码因为篇幅问题详情请见gitte【在lesson60/1.echo_server路径下】
最佳实践所得结论
- 云服务器的公网ip是禁止被显示绑定的 !
你看到的 106.54.172.42 是对外使用的公网地址,但服务器操作系统内部可能并没有这个地址。
cpp
外部电脑
|
| 访问 106.54.172.42:8080
v
腾讯云公网网关/NAT
|
| 转换为服务器私网地址,例如 10.0.0.5:8080
v
云服务器
- 在服务器开发的时候,不建议你显示绑定ip地址(实践所得结论)
- 云服务器的公网ip,是禁止被显式绑定的 但不等于 云服务器的公网ip不会被使用。(客户端依然可以使用该IP发送信息,不过切记防火墙权限问题)
这是因为客观上 : 用户 -> 云公网 IP:端口 -> 云平台映射/NAT -> 云服务器私网 IP:端口 -> 你的程序。
如果 在服务端显示绑定 IP地址 那么只能拿发送给自己绑定的IP报文,就是说即使别的IP指向服务端 客户端发送给未能绑定服务器但指向服务器的报文,也依然拿不到!
云服务器内存在的三种IP
- 主机自身
127.0.0.1 - 服务器内的局域网IP,因为局域网内部存在多个主机!

- 公网IP(没有配置到机器内部,这个我们无法查询 ,云服务器厂商收到报文后会转发到你的机器上)
为何绑定云服务器公网IP会失败?怎么解决这个问题
因为 bind() 的含义是:
要求操作系统把程序绑定到本机拥有的某个 IP 地址上。
而云服务器公网 IP 通常只是云平台网络层的映射地址,并没有配置在 Linux 网卡上:
text
Linux 网卡实际拥有:10.0.0.5
云平台对外映射:106.54.172.42
当程序执行:
c
bind(..., "106.54.172.42", ...);
Linux 会检查本机网卡,发现没有 106.54.172.42,于是失败!
正确做法:
c
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
它表示监听本机所有网卡地址:
text
外部访问 106.54.172.42:8080
↓
腾讯云转发
↓
服务器 10.0.0.5:8080
↓
程序监听 INADDR_ANY
此时 测试公网 IP,需要在另一台电脑上运行,就可以顺利测试成功了!
3. V2版本- DictServer
这个版本本质就是给服务端再添加一个字典翻译的处理程序(代码详情请见gitte)
V2、V3 版本都在
lesson62目录下!
4. V3版本- 简单聊天室
代码详情请见gitte
V2、V3 版本都在
lesson62目录下!
4.1 核心思路与流程图
我们还可以将服务端接入线程池,使其变成UDP 多人聊天室服务器
核心思路:
cpp
多个客户端发消息
↓
server_udp 统一接收消息
↓
把"转发消息"封装成任务
↓
交给线程池处理
↓
线程池执行 route_message()
↓
把消息转发给所有在线客户端(客户端双线程负责收、发)
#mermaid-svg-58AFNBz2M0fBbyGe{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-58AFNBz2M0fBbyGe .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-58AFNBz2M0fBbyGe .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-58AFNBz2M0fBbyGe .error-icon{fill:#552222;}#mermaid-svg-58AFNBz2M0fBbyGe .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-58AFNBz2M0fBbyGe .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-58AFNBz2M0fBbyGe .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-58AFNBz2M0fBbyGe .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-58AFNBz2M0fBbyGe .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-58AFNBz2M0fBbyGe .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-58AFNBz2M0fBbyGe .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-58AFNBz2M0fBbyGe .marker{fill:#333333;stroke:#333333;}#mermaid-svg-58AFNBz2M0fBbyGe .marker.cross{stroke:#333333;}#mermaid-svg-58AFNBz2M0fBbyGe svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-58AFNBz2M0fBbyGe p{margin:0;}#mermaid-svg-58AFNBz2M0fBbyGe .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-58AFNBz2M0fBbyGe .cluster-label text{fill:#333;}#mermaid-svg-58AFNBz2M0fBbyGe .cluster-label span{color:#333;}#mermaid-svg-58AFNBz2M0fBbyGe .cluster-label span p{background-color:transparent;}#mermaid-svg-58AFNBz2M0fBbyGe .label text,#mermaid-svg-58AFNBz2M0fBbyGe span{fill:#333;color:#333;}#mermaid-svg-58AFNBz2M0fBbyGe .node rect,#mermaid-svg-58AFNBz2M0fBbyGe .node circle,#mermaid-svg-58AFNBz2M0fBbyGe .node ellipse,#mermaid-svg-58AFNBz2M0fBbyGe .node polygon,#mermaid-svg-58AFNBz2M0fBbyGe .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-58AFNBz2M0fBbyGe .rough-node .label text,#mermaid-svg-58AFNBz2M0fBbyGe .node .label text,#mermaid-svg-58AFNBz2M0fBbyGe .image-shape .label,#mermaid-svg-58AFNBz2M0fBbyGe .icon-shape .label{text-anchor:middle;}#mermaid-svg-58AFNBz2M0fBbyGe .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-58AFNBz2M0fBbyGe .rough-node .label,#mermaid-svg-58AFNBz2M0fBbyGe .node .label,#mermaid-svg-58AFNBz2M0fBbyGe .image-shape .label,#mermaid-svg-58AFNBz2M0fBbyGe .icon-shape .label{text-align:center;}#mermaid-svg-58AFNBz2M0fBbyGe .node.clickable{cursor:pointer;}#mermaid-svg-58AFNBz2M0fBbyGe .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-58AFNBz2M0fBbyGe .arrowheadPath{fill:#333333;}#mermaid-svg-58AFNBz2M0fBbyGe .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-58AFNBz2M0fBbyGe .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-58AFNBz2M0fBbyGe .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-58AFNBz2M0fBbyGe .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-58AFNBz2M0fBbyGe .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-58AFNBz2M0fBbyGe .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-58AFNBz2M0fBbyGe .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-58AFNBz2M0fBbyGe .cluster text{fill:#333;}#mermaid-svg-58AFNBz2M0fBbyGe .cluster span{color:#333;}#mermaid-svg-58AFNBz2M0fBbyGe div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-58AFNBz2M0fBbyGe .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-58AFNBz2M0fBbyGe rect.text{fill:none;stroke-width:0;}#mermaid-svg-58AFNBz2M0fBbyGe .icon-shape,#mermaid-svg-58AFNBz2M0fBbyGe .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-58AFNBz2M0fBbyGe .icon-shape p,#mermaid-svg-58AFNBz2M0fBbyGe .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-58AFNBz2M0fBbyGe .icon-shape .label rect,#mermaid-svg-58AFNBz2M0fBbyGe .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-58AFNBz2M0fBbyGe .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-58AFNBz2M0fBbyGe .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-58AFNBz2M0fBbyGe :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} UDP 聊天服务器
多个 UDP 客户端
message
message
message
message
sendto() 转发
sendto() 转发
sendto() 转发
sendto() 转发
客户端 A
客户端 B
客户端 C
客户端 D
server_udp
recvfrom() 接收消息
创建转发任务
保存消息内容和发送者地址
线程池
多个工作线程
route_message()
遍历在线客户端
线程池的作用是: server_udp 专注于快速接收 UDP 数据;较耗时的"组织、转发、广播"工作交给工作线程处理,避免接收消息被阻塞。
再来张图:
同时因为聊天中 你不可能发一条消息收一条消息(recvfrom是阻塞等待的)所以客户端我们也可以用多线程的方式,一个收一个听!

4.2 服务端
服务端的核心职责可以拆成两部分:
text
1. 收到客户端消息
2. 将消息广播给其他在线客户端(线程池做)
流程如下:
text
客户端发来 UDP 报文
↓
server_udp 调用 recvfrom()
↓
得到:消息内容 + 客户端 IP + 客户端端口
↓
将该客户端记录为在线用户
↓
创建 route_message() 转发任务
↓
交给线程池执行
↓
遍历在线用户并调用 sendto()
↓
其他客户端收到消息
1. 服务端创建 UDP Socket
服务端首先创建 UDP 套接字:
cpp
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
参数含义:
text
AF_INET 使用 IPv4
SOCK_DGRAM 使用 UDP 协议
0 让系统自动选择对应协议
接着绑定端口:
cpp
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(8080);
local.sin_addr.s_addr = htonl(INADDR_ANY);
bind(sockfd, (struct sockaddr *)&local, sizeof(local));
其中:
cpp
htonl(INADDR_ANY)
表示监听服务器本机的所有网卡地址。部署到云服务器时,不建议将程序直接绑定到云厂商提供的公网 IP,因为公网 IP 往往是云平台映射的地址,未必真实存在于 Linux 网卡中。
2. 接收客户端消息
服务端循环调用 recvfrom():
cpp
char buffer[1024];
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr *)&client_addr,
&client_len);
if (n > 0)
{
buffer[n] = '\0';
}
此时服务器得到了两类信息:
text
消息内容:buffer
发送者信息:client_addr
例如:
text
客户端 A:192.168.1.10:50001
消息内容:hello
对于 UDP 聊天室,客户端不需要显式登录。只要服务器首次收到该地址发来的 UDP 数据,就可以暂时认为该客户端在线。
3. 维护在线用户列表
服务端通常维护一个在线客户端列表:
cpp
std::vector<sockaddr_in> online_users;
当收到消息后,检查客户端地址是否已存在:
text
不存在:加入在线用户列表
已存在:说明该用户仍在线
客户端地址不能只比较 IP,还要比较端口:
text
IP 相同、端口相同,才是同一个 UDP 客户端
原因是多台客户端可能处于同一局域网中,对服务器而言,它们可能通过不同端口通信。
4. 为什么要使用线程池
如果服务器收到一条消息后,直接在接收线程中广播:
text
recvfrom()
↓
遍历所有用户
↓
多次 sendto()
↓
再回去 recvfrom()
当在线用户较多时,广播过程会占用接收线程。此时新的 UDP 数据可能来不及处理,甚至因接收缓冲区满而丢失。
因此,将"转发消息"交给线程池:
text
主线程:
负责快速 recvfrom() 接收消息
工作线程:
负责 route_message() 广播消息
对应结构:
text
server_udp
↓
接收 message
↓
ThreadPool::Enqueue(route_message)
↓
线程池工作线程执行任务
↓
sendto() 广播消息
5. route_message() 的职责
route_message() 可以理解为聊天室中的"消息路由器"。
它拿到一条消息后:
text
1. 获取在线用户列表
2. 遍历每一个客户端
3. 使用 sendto() 发送消息
4. 可选择跳过消息发送者自己
代码如下:
cpp
void route_message(const std::string &message,
const sockaddr_in &sender)
{
LockGuard guard(online_users_mutex);
for (const auto &user : online_users)
{
if (user.sin_addr.s_addr == sender.sin_addr.s_addr &&
user.sin_port == sender.sin_port)
{
continue;
}
sendto(sockfd, message.c_str(), message.size(), 0,
(struct sockaddr *)&user, sizeof(user));
}
}
在线用户列表是共享资源:
text
接收线程:可能添加新用户
工作线程:可能遍历用户并广播
因此必须使用互斥锁保护,否则可能发生:
text
一个线程正在遍历 vector
另一个线程向 vector 插入用户
程序崩溃或数据错乱
4.3 客户端
客户端的任务比较明确:
text
发送线程:读取用户输入,并发送给服务器
接收线程:等待服务器消息,并显示在终端
如果只使用一个线程,程序会遇到问题:
text
程序正在 getline() 等待用户输入
↓
此时服务器发来消息
↓
客户端无法及时 recvfrom() 接收并显示
因此客户端需要两个线程。
1. 发送线程
发送线程不断读取标准输入:
cpp
void SendMessage()
{
InetAddr serveraddr(server_port, server_ip);
while (true)
{
std::string message;
std::cout << "Please Enter# " << std::flush;
std::getline(std::cin, message);
if (message.empty())
{
continue;
}
ssize_t n = sendto(sockfd, message.c_str(), message.size(), 0,
(struct sockaddr *)serveraddr.GetNetAddress(),
serveraddr.Len());
if (n < 0)
{
perror("sendto");
}
}
}
输入:
text
Please Enter# hello everyone
客户端就会将 hello everyone 发送给服务器。
首次调用 sendto() 时,客户端即使没有调用 bind(),Linux 也会自动为该 UDP Socket 选择一个临时本地端口。
2. 接收线程
接收线程不断等待服务器广播的消息:
cpp
void RecvMessage()
{
while (true)
{
char buffer[1024];
struct sockaddr_in server_addr;
socklen_t len = sizeof(server_addr);
ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr *)&server_addr, &len);
if (n < 0)
{
perror("recvfrom");
continue;
}
buffer[n] = '\0';
std::cerr << buffer << std::endl;
}
}
使用 std::cerr 输出的一个常见原因是:可以让聊天消息与用户输入提示分开处理。
但如果将标准错误重定向到命名管道:
bash
./client_udp 服务器IP 端口 2> chat_fifo
必须有另一个程序读取管道:
bash
cat chat_fifo
否则写入命名管道可能阻塞。普通 .txt 文件不需要读取端,但命名管道必须存在读写双方。
5 .一次完整聊天流程
假设客户端 A、B、C 都已经向服务器发过消息,服务器已记录它们的地址。
现在客户端 A 输入:
text
hello
完整过程如下:
text
客户端 A 的发送线程
↓
getline() 读取 hello
↓
sendto() 发送给服务器
↓
服务器 recvfrom() 收到 hello
↓
服务器得到客户端 A 的 IP 和端口
↓
创建 route_message 任务
↓
任务加入线程池
↓
某个工作线程取出任务
↓
遍历在线客户端 B、C
↓
sendto() 将 hello 发给 B、C
↓
客户端 B、C 的接收线程 recvfrom()
↓
std::cerr 输出 hello
5. 一些补充内容
5.1 netstat指令
netstat 用来查看系统的网络连接、监听端口、路由表和网卡统计信息。
Linux 中常见格式:
bash
netstat [选项]
| 选项 | 含义 |
|---|---|
-a |
显示所有连接和监听端口 |
-n |
直接显示数字 IP 和端口 |
-t |
只显示 TCP |
-u |
只显示 UDP |
-l |
只显示监听状态 |
-p |
显示对应进程和 PID |
-r |
显示路由表 |
-i |
显示网卡信息 |
-s |
显示网络协议统计 |
-c |
持续刷新显示 |
-4 |
只显示 IPv4 |
-6 |
只显示 IPv6 |
常用组合:
cpp
netstat -anup//作用:查看所有 UDP 端口以及对应的进程。
netstat -antp//作用:查看所有 TCP 连接、监听端口以及对应的进程。

5.2 ssh与sshd是什么
sshd 是 SSH 服务端程序,全称是:
text
SSH daemon
它运行在 Linux 服务器上,负责接收其他设备发起的 SSH 远程连接。
例如你执行:
bash
ssh root@106.54.172.42
连接过程大致是:
text
你的电脑
|
| SSH 连接
v
服务器 106.54.172.42:22
|
v
sshd
|
v
登录 Shell
常见特点
- 默认监听 TCP
22端口- 负责用户身份认证
- 支持密码登录和密钥登录
- 登录成功后提供远程命令行
- 通常以后台服务形式运行
查看 sshd 是否监听:
bash
netstat -antp | grep sshd
可能看到:
text
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 1234/sshd
这表示 sshd 正在监听所有 IPv4 网卡的 22 端口。

需要注意:
ssh:客户端命令,用来连接别人sshd:服务器程序,等待别人连接
例如:
text
ssh -> 发起连接
sshd -> 接收连接
sshd这样的服务,绑定在22号端口,我们能用xshell远程登上云服务器,是因为我们云服务器上启动了绑定22号端口的这个服务!我们通过ssh访问服务器,从而实现"云服务",而这个程序都是云服务厂家提前打开的!
cpp
Xshell/ssh
↓
连接公网 IP:22
↓
安全组、防火墙检查
↓
云平台将公网 IP 映射到服务器私网 IP
↓
sshd 接收连接
↓
SSH 加密握手(目前了解)
↓
用户名密码或密钥认证(目前了解)
↓
创建 Shell 会话
↓
执行 ls
↓
命令结果通过 SSHD 加密返回
↓
网络传输
↓
Xshell 解密
↓
Xshell 界面显示结果
通过 Xshell 使用 SSH 协议访问云服务器,云服务器上的 sshd 在 TCP 22 端口监听并处理连接,从而实现远程登录和管理云服务器。
流程为:
cpp
客户端发送命令:
Xshell 加密 -> sshd 解密
服务器返回结果:
sshd 加密 -> Xshell 解密
5.3 在其他系统进行套接字编程访问云服务器
OS内核是不一样的,但是网络协议栈所有系统都是一致的,所以核心接口应该是一致的!
以博主的Macos为例子(可以利用ai生成代码):



