目录
[1.1. IP地址概述](#1.1. IP地址概述)
[1.2. 端口号的作用](#1.2. 端口号的作用)
[1.3. 端口号与进程PID的关系](#1.3. 端口号与进程PID的关系)
[1.4. 传输层协议的选择](#1.4. 传输层协议的选择)
[1.5. 网络字节序与主机字节序](#1.5. 网络字节序与主机字节序)
[2. Socket 套接字](#2. Socket 套接字)
[2.1 Socket 常见 API](#2.1 Socket 常见 API)
[2.2 sockaddr 结构体](#2.2 sockaddr 结构体)
[2.3. 创建套接字](#2.3. 创建套接字)
[2.4. 绑定 IP 地址和端口号](#2.4. 绑定 IP 地址和端口号)
[2.5. 使用 INADDR_ANY 绑定任意可用 IP 地址](#2.5. 使用 INADDR_ANY 绑定任意可用 IP 地址)
[3.字符串回响 UDP 服务](#3.字符串回响 UDP 服务)
[1. 创建 server.hpp 文件 (服务器头文件)](#1. 创建 server.hpp 文件 (服务器头文件))
[2. 创建 server.cc 文件 (服务器源文件)](#2. 创建 server.cc 文件 (服务器源文件))
[3. 创建 client.hpp 文件 (客户端头文件)](#3. 创建 client.hpp 文件 (客户端头文件))
[4. 创建 client.cc 文件 (客户端源文件)](#4. 创建 client.cc 文件 (客户端源文件))
[5. 创建 Makefile](#5. 创建 Makefile)
[6. 远程 Bash 执行功能](#6. 远程 Bash 执行功能)
[7. 安全检查](#7. 安全检查)
[8. 服务器启动并执行命令](#8. 服务器启动并执行命令)
[9. 启动服务器](#9. 启动服务器)
[4. 多人聊天室(UDP协议)](#4. 多人聊天室(UDP协议))
[4.1 核心功能](#4.1 核心功能)
[4.2 程序结构](#4.2 程序结构)
[4.3 引入环形队列](#4.3 引入环形队列)
[4.4 引入用户信息](#4.4 引入用户信息)
[4.5 引入多线程](#4.5 引入多线程)
[4.6 客户端多线程化](#4.6 客户端多线程化)
[4.7 编译和运行](#4.7 编译和运行)
🌦️正文
1.预备知识
1.1. IP地址概述
在网络基础中,IP 地址是全球范围内标识主机的唯一标识符。我们利用 IP 地址来定位公网中的设备,进而实现跨越路由器进行远程通信------例如,从主机 A 发送信息到主机 Z。

然而,仅仅拥有 IP 地址只能帮助我们定位目标主机,但无法准确到达主机中的特定进程。为此,端口号的引入成为解决这一问题的关键。

主机内有多个进程在运行,实际的网络通信是发生在**不同主机的进程之间,**而并非主机与主机直接通信。因此,端口号成为实现进程间通信的必备工具。
1.2. 端口号的作用
端口号是一个 2 字节的整数,用于标识网络中的进程,其范围为 [0, 65535] 。它帮助定位到特定进程,确保通信数据能够准确无误地到达目标进程。
若把进程间通信视为一种形式的"消息传递",我们可以将网络通信看作是进程间通过网络进行的通信。在传统的进程间通信中,我们通过共享内存、管道等方式来实现;而在网络通信中,进程间的通信则通过端口号进行管理。
服务器的防火墙实际上就是通过端口号进行限制,只有开放的端口才能允许进程进行网络通信。
1.3. 端口号与进程PID的关系
端口号和进程 PID 都可以标识一个进程,但为什么不直接用 PID 而是使用端口号呢? 原因在于,进程 PID 属于操作系统内的进程管理范畴,而网络标准应该独立于操作系统的实现。因此,直接使用 PID 会使得网络标准与操作系统的管理过于耦合。
网络中的端口号与操作系统的 PID 是两个不同的概念。端口号标识的是网络通信中目标进程,而 PID 属于操作系统的内部管理信息,因此使用端口号作为进程标识更加灵活且符合网络独立性原则。
一个进程可以绑定多个端口号吗?一个端口号可以被多个进程绑定吗?
端口号的主要作用是与 IP 地址配合,标识网络中进程的唯一性。 若一个进程绑定多个端口号,它仍然可以保持唯一性,因为无论使用哪个端口号,信息都会被定向到该进程;然而,如果一个端口号被多个进程绑定,就会存在信息无法准确传递的问题,因为系统无法区分该端口号应该将数据交给哪个进程,从而导致通信的二义性。
因此,一个进程可以绑定多个端口号 ,但一个端口号不能被多个进程绑定。如果端口号已经被某个进程占用,其他进程在尝试绑定时会收到"端口已被占用"的错误提示。
操作系统如何根据端口号定位进程?
这个过程实现起来比较直接。操作系统通常会创建一张哈希表,维护端口号与进程 PID 之间的映射关系。当数据传输到目标主机时,操作系统根据目标端口号查找哈希表,准确定位到该端口号所对应的进程 PID,从而确保信息准确地交给对应的进程。
1.4. 传输层协议的选择
传输层有两种主流协议:TCP 和 UDP。它们各自有不同的特性,适用于不同的应用场景。
-
TCP 协议 :是一个有连接的协议 ,保证可靠的数据传输,适用于要求高可靠性和准确性的应用场景,如网页浏览、文件传输等。
-
UDP 协议 :无连接、不可靠,但传输速度较快。适用于对实时性要求高、数据丢失可以容忍的场景,如视频直播、即时通讯等。
总结来说,如果你无法判断使用哪种协议,优先考虑 TCP 协议。如果在可靠性上没有特别要求,并且对传输速度有较高需求,可以选择 UDP。
1.5. 网络字节序与主机字节序
在计算机内部,数据存储有两种方式**:大端字节序和小端字节序** 。具体来说,大端字节序将数据的高权值字节存储在低地址,而小端字节序则将高权值字节存储在高地址。不同计算机可能采用不同的字节序存储方式,这就引发了网络通信中的问题。

为了解决不同字节序系统间的数据兼容性问题,**网络协议采用了统一的网络字节序(即大端字节序)。**无论发送方或接收方的字节序如何,都能通过协议统一处理,确保数据的正确传输。
解决方案:
-
解决方案 1:数据传输时添加字节序标志位,接收方根据标志位进行转换,但这种方式增加了额外的开销。
-
解决方案 2 :通过统一标准(如大端字节序)来处理数据存储和传输,从根本上解决兼容问题。
在 TCP/IP 协议中,采用了第二种方案,即统一使用大端字节序。这种方式大大简化了跨平台数据传输的复杂性,避免了因字节序不同而导致的错误。
可以使用相关库函数进行字节序转换,如下所示:
cpp
#include <arpa/inet.h>
// 主机字节序转网络字节序
uint32_t htonl(uint32_t hostlong);
uint32_t htons(uint32_t hostshort);
// 网络字节序转主机字节序
uint32_t ntohl(uint32_t netlong);
uint32_t ntohs(uint32_t netshort);
2. Socket 套接字
2.1 Socket 常见 API
Socket 套接字提供了一些常用的接口,用于实现不同类型的网络通信。以下是一些常见的 API 接口:
cpp
#include <sys/types.h>
#include <sys/socket.h>
// 创建 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);
在这些 API 中,sockaddr 结构体频繁出现,它是网络通信的关键组成部分,也用于本地通信。
Socket 套接字通过文件描述符的方式来描述 sockaddr
结构体,使得其能够跨平台工作,无论是网络通信还是本地通信,都能有效利用相同的结构体来处理。
2.2 sockaddr 结构体
Socket 网络通信标准是 POSIX 通信标准的一部分,旨在实现跨平台的兼容性。这使得开发者可以**在不同平台上使用相同的通信标准。sockaddr
**结构体是为了兼顾网络通信和本地通信而设计的。
socket 套接字通过 sockaddr 结构体来处理这两种通信方式。在此基础上,衍生出了两种常用的结构体:
-
sockaddr_in:用于网络通信,存储 IP 地址和端口信息。
-
sockaddr_un:用于本地通信,通过路径名来进行通信,类似于命名管道。
根据地址类型(16 位),可以判断是进行网络通信还是本地通信。对于网络通信,我们需要提供 IP 地址和端口号,而本地通信只需要提供一个路径名,通过文件读写进行通信。

在进行套接字编程时,socket
接口 的参数通常是 sockaddr*
类型,这意味着我们可以传入 &sockaddr_in
来进行网络通信,也可以传入 **&sockaddr_un
**进行本地通信。传递时,只需要进行适当的强制类型转换,这是 C 语言中的多态应用,确保了接口的通用性。
为什么不使用 void*
作为参数?
在设计 POSIX 标准时,C 语言并没有支持 void*
类型。为了确保标准的兼容性,接口设计者避免使用 void*
,从而使得该接口能够在后续语言不支持该类型时保持兼容。
有关 sockaddr_in
结构体的更详细信息,将在后续的代码实现部分进行讲解。
2.3. 创建套接字
在网络编程中,**socket()**函数用于创建一个套接字,这个套接字用于进程间的通信。在 UDP 通信中,我们通过此套接字发送和接收数据。
socket
函数说明:
cpp
#include <sys/types.h>
#include <sys/socket.h>
// 创建套接字
int socket(int domain, int type, int protocol);
参数解释:
-
domain
:选择通信的域(比如AF_INET
代表 IPv4)。 -
type
:选择数据传输方式(SOCK_DGRAM 表示数据报通信,适用于 UDP ,SOCK_STREAM 适用于TCP)。 -
protocol
:通常设为 0,系统会根据type
自动选择合适的协议(对于SOCK_DGRAM
,系统会自动选择 UDP)。
返回值:
-
成功时返回一个非负整数,即套接字的文件描述符。
-
失败时返回 -1,并通过
errno
提供错误信息。

代码示例:
cpp
sock_ = socket(AF_INET, SOCK_DGRAM, 0); // 创建 UDP 套接字
if (sock_ == -1) {
std::cerr << "Socket creation failed: " << strerror(errno) << std::endl;
exit(1);
}
2.4. 绑定 IP 地址和端口号
使用 bind()
函数将套接字与本地地址(IP 地址和端口)绑定。通过这个操作,服务器就能够接收来自指定 IP 地址和端口的请求。
bind
函数说明:
cpp
#include <sys/types.h>
#include <sys/socket.h>
// 绑定 IP 地址和端口
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数解释:
-
sockfd :套接字描述符(由 socket() 创建)。
-
addr :指定通信的地址结构体,通常为 sockaddr_in 结构,包含 IP 地址和端口号。
-
addrlen :地址结构的大小。
返回值:
-
成功时返回 0。
-
失败时返回 -1,并设置
errno
。
代码示例:
cpp
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 清空结构体
local.sin_family = AF_INET; // 设置为 IPv4
local.sin_port = htons(port_); // 将主机字节序转换为网络字节序
local.sin_addr.s_addr = inet_addr(ip_.c_str()); // 将 IP 地址转换为网络字节序
if (bind(sock_, (const sockaddr*)&local, sizeof(local)) == -1) {
std::cerr << "Binding IP and Port failed: " << strerror(errno) << std::endl;
exit(1);
}
使用的是 sockaddr_in
结构体,要想使用该结构体,还得包含下面这两个头文件
cpp
#include <netinet/in.h>
#include <arpa/inet.h>
sockaddr_in
结构体解释:
cpp
struct sockaddr_in {
sa_family_t sin_family; // 地址族,通常是 AF_INET
in_port_t sin_port; // 端口号,使用 htons 转换为网络字节序
struct in_addr sin_addr; // IP 地址,使用 inet_addr 转换为网络字节序
unsigned char sin_zero[8]; // 填充,确保结构体大小与 sockaddr 匹配
};

首先来看看 16
位地址类型 ,转到定义可以发现它是一个宏函数 ,并且使用了 C语言 中一个非常少用的语法 ##
(将两个字符串拼接);
cpp
/* POSIX.1g specifies this type name for the `sa_family' member. */
typedef unsigned short int sa_family_t;
/* This macro is used to declare the initial common members
of the data types used for socket addresses, `struct sockaddr',
`struct sockaddr_in', `struct sockaddr_un', etc. */
#define __SOCKADDR_COMMON(sa_prefix) \
sa_family_t sa_prefix##family
当给 _SOCKADDR_COMMON 传入 sin 参数后,经过**##**字符串拼接、宏替换等操作后,会得到这样一个类型
cpp
sa_family_t sin_family;
sa_family_t 是一个无符号短整数,占 16
位,sin_family 字段就是 16
位地址类型 了
接下来看看 端口号 ,转到定义,发现 in_port_t 类型是一个 16
位无符号整数,同样占 2
字节,正好符合 端口号 的取值范围 [0, 65535]
cpp
/* Type to represent a port. */
typedef uint16_t in_port_t;
最后再来看看 IP
地址 ,同样转到定义,发现 in_addr 中包含了一个 32
位无符号整数,占 4
字节,也就是 IP
地址 的大小
cpp
/* Internet address. */
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
了解完 sockaddr_in 结构体中的内容后,就可以创建该结构体了,再定义该结构体后,需要清空,确保其中的字段干净可用
将变量置为
0
可用使用bzero
函数
cpp
#include <cstrins> // bzero 函数的头文件
struct sockaddr_in local;
bzero(&local, sizeof(local));
获得一个干净可用的 sockaddr_in
结构体后,可以正式绑定 IP
地址 和 端口号 了
注:作为服务器,需要确定自己的端口号,我这里设置的是 8888
主机字节序转换为网络字节序:
-
htons():将 16 位的端口号从主机字节序转换为网络字节序。
-
inet_addr():将点分十进制的 IP 地址字符串转换为网络字节序的整数形式。
2.5. 使用 INADDR_ANY
绑定任意可用 IP 地址
在某些情况下,比如在云服务器或有多个网络接口的机器上,云服务器是不允许直接绑定公网 IP 的,解决方案是在绑定 IP 地址时,让其选择绑定任意可用 IP 地址。 服务器可以使用 INADDR_ANY
来绑定任意可用的 IP 地址。这意味着服务器会监听所有网络接口上的请求。
修改后的服务器头文件:
cpp
class UdpServer {
public:
// 构造函数
UdpServer(uint16_t port = default_port) : port_(port) {}
void InitServer() {
// 创建 UDP 套接字
sock_ = socket(AF_INET, SOCK_DGRAM, 0);
if (sock_ == -1) {
std::cerr << "Socket creation failed: " << strerror(errno) << std::endl;
exit(1);
}
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 清空结构体
local.sin_family = AF_INET; // 设置为 IPv4
local.sin_port = htons(port_); // 设置端口号
local.sin_addr.s_addr = INADDR_ANY; // 绑定任意可用 IP 地址
// 绑定 IP 地址和端口号
if (bind(sock_, (const sockaddr*)&local, sizeof(local)) == -1) {
std::cerr << "Binding failed: " << strerror(errno) << std::endl;
exit(1);
}
}
private:
int sock_;
uint16_t port_;
};
3.字符串回响 UDP 服务
- 核心功能
这个程序实现了客户端与服务器之间的基本通信。客户端将数据发送给服务器,服务器接收到后进行回显,类似于 echo
命令。整个过程通过 UDP 协议进行,服务器不保存任何连接状态,保证了高效的无连接数据传输。
- 程序结构
程序包含以下四个文件:
-
server.hpp
:服务器端头文件,定义了UdpServer
类。 -
server.cc
:服务器端源文件,包含业务逻辑。 -
client.hpp
:客户端头文件,定义了UdpClient
类。 -
client.cc
:客户端源文件,处理与服务器的通信。
1. 创建 server.hpp
文件 (服务器头文件)
cpp
#pragma once
#include <iostream>
#include <string>
namespace nt_server {
class UdpServer {
public:
UdpServer(const std::string& ip, uint16_t port);
~UdpServer();
void InitServer();
void StartServer();
private:
int sock_; // 套接字描述符
uint16_t port_; // 端口号
std::string ip_; // 服务器IP地址
};
}
2. 创建 server.cc
文件 (服务器源文件)
cpp
#include "server.hpp"
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <iostream>
#include <cstdlib>
using namespace std;
using namespace nt_server;
UdpServer::UdpServer(const std::string& ip, uint16_t port) : ip_(ip), port_(port) {}
UdpServer::~UdpServer() {}
void UdpServer::InitServer() {
sock_ = socket(AF_INET, SOCK_DGRAM, 0); // 创建UDP套接字
if (sock_ == -1) {
std::cerr << "Socket creation failed!" << std::endl;
exit(1);
}
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port_);
local.sin_addr.s_addr = inet_addr(ip_.c_str());
if (bind(sock_, (struct sockaddr*)&local, sizeof(local)) < 0) {
std::cerr << "Binding failed!" << std::endl;
exit(1);
}
}
void UdpServer::StartServer() {
char buffer[1024];
struct sockaddr_in client;
socklen_t len = sizeof(client);
while (true) {
ssize_t n = recvfrom(sock_, buffer, sizeof(buffer), 0, (struct sockaddr*)&client, &len);
if (n < 0) {
std::cerr << "Failed to receive message!" << std::endl;
continue;
}
buffer[n] = '\0';
std::cout << "Received: " << buffer << std::endl;
// 将接收到的消息回传给客户端
ssize_t sent = sendto(sock_, buffer, n, 0, (struct sockaddr*)&client, len);
if (sent < 0) {
std::cerr << "Failed to send message!" << std::endl;
}
}
}
3. 创建 client.hpp
文件 (客户端头文件)
cpp
#pragma once
#include <iostream>
#include <string>
namespace nt_client {
class UdpClient {
public:
UdpClient(const std::string& ip, uint16_t port);
~UdpClient();
void InitClient();
void StartClient();
private:
int sock_; // 套接字描述符
std::string server_ip_; // 服务器IP地址
uint16_t server_port_; // 服务器端口号
};
}
4. 创建 client.cc
文件 (客户端源文件)
cpp
#include "client.hpp"
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <iostream>
#include <cstdlib>
using namespace std;
using namespace nt_client;
UdpClient::UdpClient(const std::string& ip, uint16_t port) : server_ip_(ip), server_port_(port) {}
UdpClient::~UdpClient() {}
void UdpClient::InitClient() {
sock_ = socket(AF_INET, SOCK_DGRAM, 0);
if (sock_ == -1) {
std::cerr << "Socket creation failed!" << std::endl;
exit(1);
}
}
void UdpClient::StartClient() {
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(server_port_);
server_addr.sin_addr.s_addr = inet_addr(server_ip_.c_str());
char message[1024];
while (true) {
std::cout << "Enter message: ";
std::cin.getline(message, sizeof(message));
ssize_t sent = sendto(sock_, message, strlen(message), 0, (struct sockaddr*)&server_addr, sizeof(server_addr));
if (sent < 0) {
std::cerr << "Failed to send message!" << std::endl;
continue;
}
char buffer[1024];
socklen_t len = sizeof(server_addr);
ssize_t n = recvfrom(sock_, buffer, sizeof(buffer), 0, (struct sockaddr*)&server_addr, &len);
if (n < 0) {
std::cerr << "Failed to receive message!" << std::endl;
continue;
}
buffer[n] = '\0';
std::cout << "Server echoed: " << buffer << std::endl;
}
}
5. 创建 Makefile
cpp
.PHONY: all
all: server client
server: server.cc
g++ -o $@ $^ -std=c++11
client: client.cc
g++ -o $@ $^ -std=c++11
.PHONY: clean
clean:
rm -rf server client
6. 远程 Bash 执行功能
我们将实现一个函数,可以执行客户端发送的 Bash 命令。我们将利用**popen
函数**来执行这些命令并返回结果。
远程命令执行函数
cpp
#include <stdio.h>
#include <string>
std::string ExecCommand(const std::string& request) {
FILE* fp = popen(request.c_str(), "r");
if (fp == nullptr) {
return "Command execution failed!";
}
std::string result;
char buffer[1024];
while (fgets(buffer, sizeof(buffer), fp) != nullptr) {
result += buffer;
}
fclose(fp);
return result;
}
7. 安全检查
为了防止执行危险命令,如 rm
、kill
等,我们需要进行安全检查,过滤掉不安全的命令。
cpp
bool checkSafe(const std::string& command) {
std::vector<std::string> unsafeCommands = {"kill", "rm", "shutdown", "mv"};
for (const auto& unsafe : unsafeCommands) {
if (command.find(unsafe) != std::string::npos) {
return false;
}
}
return true;
}
8. 服务器启动并执行命令
将安全检查与命令执行结合,在服务器端接收命令并执行。若命令安全,执行并返回结果;若命令不安全,返回错误信息。
cpp
std::string ExecCommandWithSafety(const std::string& request) {
if (!checkSafe(request)) {
return "Unsafe command! Refused to execute.";
}
return ExecCommand(request);
}
9. 启动服务器
在 UdpServer
类中,我们将 ExecCommandWithSafety
作为回调函数传递进去,实现命令执行的功能。
cpp
void UdpServer::StartServer() {
char buffer[1024];
struct sockaddr_in client;
socklen_t len = sizeof(client);
while (true) {
ssize_t n = recvfrom(sock_, buffer, sizeof(buffer), 0, (struct sockaddr*)&client, &len);
if (n < 0) {
std::cerr << "Failed to receive message!" << std::endl;
continue;
}
buffer[n] = '\0';
std::cout << "Received: " << buffer << std::endl;
std::string result = ExecCommandWithSafety(buffer);
ssize_t sent = sendto(sock_, result.c_str(), result.size(), 0, (struct sockaddr*)&client, len);
if (sent < 0) {
std::cerr << "Failed to send message!" << std::endl;
}
}
}
4. 多人聊天室(UDP协议)
4.1 核心功能
这段程序实现了一个基于 UDP 协议的多人聊天室。所有参与聊天室的用户都可以发送和接收消息。服务器充当消息接收与转发的角色,将用户发送的消息广播给其他所有用户。
4.2 程序结构
聊天室的设计遵循生产者-消费者模型:
-
生产者:负责接收消息并将其放入环形队列中。
-
消费者:负责从环形队列中提取消息并广播给所有已知用户。
每个用户(客户端)都有一个独立的线程用于发送消息和接收消息。服务器则有两个线程:一个用于接收消息,一个用于广播消息。
4.3 引入环形队列
为了实现生产者-消费者模型,使用环形队列(RingQueue
)来存储和管理消息。消息首先被生产者(接收线程)放入队列,然后消费者(广播线程)从队列中取出并发送给其他客户端。
环形队列类 RingQueue.hpp
(简化示例):
cpp
#pragma once
#include <queue>
#include <mutex>
#include <condition_variable>
template <typename T>
class RingQueue {
public:
void Push(const T& item) {
std::lock_guard<std::mutex> lock(mtx_);
queue_.push(item);
cond_.notify_one(); // 通知消费者线程
}
bool Pop(T* item) {
std::unique_lock<std::mutex> lock(mtx_);
cond_.wait(lock, [this] { return !queue_.empty(); });
*item = queue_.front();
queue_.pop();
return true;
}
private:
std::queue<T> queue_;
std::mutex mtx_;
std::condition_variable cond_;
};
4.4 引入用户信息
为了区分不同用户,使用用户的 IP + Port
作为唯一标识符。在服务器上维护一个哈希表,保存每个用户的信息。这个表格用于存储用户地址(sockaddr_in
结构体)。
cpp
std::unordered_map<std::string, sockaddr_in> userTable_;
每当有用户发送消息时,首先检查该用户是否已加入聊天室。如果是新用户,则将其添加到 userTable_
中。
4.5 引入多线程
我们使用两个线程:
-
生产者线程:接收消息并将其放入环形队列。
-
消费者线程:从环形队列中获取消息并将其广播给所有用户。
服务器端多线程实现:
cpp
class UdpServer {
public:
UdpServer(uint16_t port = default_port) : port_(port) {
pthread_mutex_init(&mtx_, nullptr);
producer_ = new Thread(1, std::bind(&UdpServer::RecvMessage, this));
consumer_ = new Thread(2, std::bind(&UdpServer::BroadcastMessage, this));
}
void StartServer() {
// 创建套接字、绑定等
// 启动线程
producer_->run();
consumer_->run();
}
void RecvMessage() {
char buff[1024];
while(true) {
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr*)&peer, &len);
if (n > 0) {
buff[n] = '\0';
std::string clientIp = inet_ntoa(peer.sin_addr);
uint16_t clientPort = ntohs(peer.sin_port);
std::string user = clientIp + "-" + std::to_string(clientPort);
if (userTable_.count(user) == 0) {
userTable_[user] = peer; // 新用户加入
}
std::string msg = "[" + clientIp + ":" + std::to_string(clientPort) + "] " + buff;
rq_.Push(msg); // 将消息推入队列
}
}
}
void BroadcastMessage() {
while(true) {
std::string msg;
rq_.Pop(&msg); // 从队列中取出消息
std::vector<sockaddr_in> userAddresses;
{
LockGuard lockguard(&mtx_);
for (const auto& user : userTable_) {
userAddresses.push_back(user.second);
}
}
// 广播消息
for (const auto& addr : userAddresses) {
sendto(sock_, msg.c_str(), msg.size(), 0, (const sockaddr*)&addr, sizeof(addr));
}
}
}
private:
int sock_;
uint16_t port_;
RingQueue<std::string> rq_;
std::unordered_map<std::string, sockaddr_in> userTable_;
pthread_mutex_t mtx_;
Thread* producer_;
Thread* consumer_;
};
4.6 客户端多线程化
客户端需要同时发送和接收消息,因此引入两个线程:
-
发送消息线程:负责向服务器发送消息。
-
接收消息线程:负责接收从服务器广播的消息。
客户端头文件:
cpp
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Thread.hpp"
namespace nt_client {
class UdpClient {
public:
UdpClient(const std::string& ip, uint16_t port)
: server_ip_(ip), server_port_(port) {
// 创建线程
recv_ = new Thread(1, std::bind(&UdpClient::RecvMessage, this));
send_ = new Thread(2, std::bind(&UdpClient::SendMessage, this));
}
~UdpClient() {
// 等待线程退出
recv_->join();
send_->join();
delete recv_;
delete send_;
}
void StartClient() {
sock_ = socket(AF_INET, SOCK_DGRAM, 0);
if (sock_ == -1) {
std::cerr << "Socket creation failed!" << std::endl;
exit(1);
}
bzero(&svr_, sizeof(svr_));
svr_.sin_family = AF_INET;
svr_.sin_addr.s_addr = inet_addr(server_ip_.c_str());
svr_.sin_port = htons(server_port_);
// 启动线程
recv_->run();
send_->run();
}
void SendMessage() {
while (true) {
std::string msg;
std::cout << "Enter message: ";
std::getline(std::cin, msg);
ssize_t n = sendto(sock_, msg.c_str(), msg.size(), 0, (const struct sockaddr*)&svr_, sizeof(svr_));
if (n == -1) {
std::cerr << "Send failed!" << std::endl;
continue;
}
}
}
void RecvMessage() {
char buff[1024];
while (true) {
ssize_t n = recvfrom(sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr*)&svr_, nullptr);
if (n > 0) {
buff[n] = '\0';
std::cout << "Received: " << buff << std::endl;
}
}
}
private:
std::string server_ip_;
uint16_t server_port_;
int sock_;
struct sockaddr_in svr_;
Thread* recv_;
Thread* send_;
};
}
4.7 编译和运行
确保在编译时链接 pthread 库:
cpp
g++ -o server server.cc -std=c++11 -lpthread
g++ -o client client.cc -std=c++11 -lpthread
总结
通过引入环形队列、哈希表、多线程等技术,实现了一个基于 UDP 协议的多人聊天室。服务器通过 recv
接收消息,并通过环形队列管理消息,消费者线程将消息广播给所有用户。客户端通过多线程实现消息的实时发送和接收,确保了聊天室的高效通信。