Linux 网络编程:套接字基础与 UDP 协议实现
一、预备知识
1.1 理解网络通信的本质
网络通信的本质是不同主机下的进程间通信。以 QQ 为例,应用层完成数据的发送和接收,而网络协议的中下三层主要解决将数据安全可靠地送到远端机器。要使用软件进行通信,必须先启动进程。
1.2 理解 IP 地址和端口号
为了找到对方主机,我们需要 IP 地址作为唯一标识。但仅有 IP 地址无法区分主机上的具体进程,因此需要端口号来标识信息应交给哪个进程处理。
端口号 (port) 是传输层协议的内容
- 端口号是一个 2 字节 16 位的整数。
- 用来标识一个进程,告诉操作系统当前的数据要交给哪一个进程来处理。
- 一个端口号只能被一个进程占用。
所以 IP 地址 + 端口号能够标识网络上的某一台主机的某一个进程。
传输层协议 (TCP 和 UDP) 的数据段中有两个端口号,分别叫做源端口号和目的端口号,描述'数据是谁发的,要发给谁'。
1.3 端口号 VS 进程 ID
PID 虽然能标识一台主机上进程的唯一性,但引入端口号是为了让系统和网络功能解耦。并非所有进程都需要网络通信,但所有进程都有 PID。端口号设计为众所周知或内置,便于客户端定位服务端。
- 一个进程可以绑定多个端口号。
- 一个端口号不能绑定多个进程。
1.4 TCP vs UDP
| 特性 | TCP (Transmission Control Protocol) | UDP (User Datagram Protocol) |
|---|---|---|
| 类型 | 传输层协议 | 传输层协议 |
| 连接 | 有连接 | 无连接 |
| 可靠性 | 可靠传输 | 不可靠传输 |
| 数据单元 | 面向字节流 | 面向数据报 |
- 有连接和无连接:连接好比打电话,先确认连接再沟通;无连接好比发送邮件,发了就行,不关心是否收到。
- 为什么 UDP 存在:可靠是有成本的,不可靠更简单。TCP 需要维护报文信息、重传策略、排序等;UDP 拿到数据包直接转发,不关心发送情况。注意,可靠的前提是网络必须连通。
1.5 网络字节序
内存中的多字节数据相对于内存地址有大端和小端之分,网络数据流同样如此。TCP/IP 协议规定,网络数据流应采用大端字节序(低地址高字节)。
为使网络程序具有可移植性,使同样的 C 代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换:
htonl: 32 位长整数从主机字节序转换为网络字节序。htons: 16 位短整数从主机字节序转换为网络字节序。ntohl,ntohs: 反向转换。
1.6 Socket 接口
Socket 常见 API:
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
// 开始监听 socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
1.7 套接字的种类
所谓套接字 (Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。
- 域间套接字(同一个机器内):
struct sockaddr_un - 原始套接字(网络工具): 绕过传输层,用于网络装包、诊断。
- 网络套接字(用户间通信):
struct sockaddr_in
为了统一参数类型,通常使用 sockaddr 结构体,根据前 16 位分辨类型,使用时需做强转。
二、UDP 编程实现
2.1 服务端
2.1.1 Init - 创建服务端
-
创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (_sockfd < 0) { // 错误处理 }- 第一个参数:套接字域 (
AF_INET为 IPv4)。 - 第二个参数:套接字类型 (
SOCK_DGRAM为 UDP)。 - 第三个参数:协议类型 (默认为 0)。
- 第一个参数:套接字域 (
-
绑定套接字
struct sockaddr_in local; bzero(&local, sizeof(local)); local.sin_family = AF_INET; local.sin_port = htons(_port); // 端口号转为网络字节序 local.sin_addr.s_addr = inet_addr(_ip.c_str()); // IP 转网络字节序 if (bind(_sockfd, (const struct sockaddr *)&local, sizeof(local)) < 0) { // 错误处理 }
2.1.2 Run - 服务器启动
UDP 不是面向字节流的,使用 recvfrom 接口接收数据,并获取客户端信息。
void Run(func_t func) {
_isrunning = true;
char inbuffer[size];
while (_isrunning) {
struct sockaddr_in client;
socklen_t len = sizeof(client);
ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0,
(struct sockaddr *)&client, &len);
if (n < 0) continue;
inbuffer[n] = 0;
std::string info = inbuffer;
std::string echo_string = func(info);
sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0,
(const sockaddr *)&client, len);
}
}
2.1.3 关于 Port 和 IP
- Port: [0-1023] 一般是系统内定的端口号,有固定的应用协议使用。
- IP: 禁止直接 bind 公网 IP(在虚拟机上可以),因为服务端机器可能有多个网卡、多个 IP。
2.1.4 查看当前网络状态
可使用 netstat 等命令查看。
2.1.5 环回地址
本地测试常用 127.0.0.1。
2.1.6 地址转换函数
基于 IPv4 的 socket 网络编程,sockaddr_in 中的成员 struct in_addr sin_addr 表示 32 位的 IP 地址。
- 字符串转
in_addr:inet_addr() in_addr转字符串:inet_ntoa()(非线程安全),inet_ntop()(推荐)
2.1.7 关于 inet_ntoa
inet_ntoa 返回结果放在静态存储区,多次调用会覆盖上一次结果,且非线程安全。多线程环境下推荐使用 inet_ntop。
2.2 UDP 客户端
-
创建客户端套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);客户端通常不需要显式 bind,由 OS 随机分配端口。
-
发送数据
sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&server, len); -
接收数据
recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);
2.3 主函数
#include"UdpServer.hpp"
#include <memory>
string Handler(const string &str) {
std::string res = "Server get a message: ";
res += str;
return res;
}
int main(int argc, char* argv[]) {
if (argc != 2) { /* Usage */ }
uint16_t port = std::stoi(argv[1]);
unique_ptr<UdpServer> svr(new UdpServer(port));
svr->Init();
svr->Run(Handler);
return 0;
}
2.4 模拟云服务器命令输入
服务端可根据传入字符串执行命令,使用 popen 函数。
std::string ExcuteCommand(const std::string &cmd) {
FILE *fp = popen(cmd.c_str(), "r");
if (nullptr == fp) return "error";
std::string result;
char buffer[4096];
while (fgets(buffer, sizeof(buffer), fp)) {
result += buffer;
}
pclose(fp);
return result;
}
2.5 实现聊天室 + 多线程
群聊需要服务端维护用户列表,并将消息广播给其他客户端。
-
检查并添加新用户
void CheckUser(const struct sockaddr_in &client, const string clientip, uint16_t clientport) { auto iter = online_user_.find(clientip); if (iter == online_user_.end()) { online_user_.insert({clientip, client}); } } -
广播消息
void Broadcast(const string &info, const string clientip, uint16_t clientport) { for (const auto &user : online_user_) { std::string message = "[" + clientip + ":" + std::to_string(clientport) + "]# " + info; socklen_t len = sizeof(user.second); sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)(&user.second), len); } }
为避免阻塞,客户端采用多线程:一个线程负责发送,一个线程负责接收。
2.6 聊天窗口
通过重定向终端输出,可以实现输入输出分离,优化聊天体验。
2.7 Windows 客户端交互
支持跨平台交互,Windows 客户端也可接入 Linux 服务端。


