最近一直在学习c++的网络编程方面的内容,为了加深记忆,巩固理解,故作代码记录,以后会不断地完善。
一、预备知识网络套接字
套接字就像“插座与插头”的配对机制,是客户端与服务端通信的桥梁。在一次典型的 TCP 网络连接中,服务端会首先创建一个“监听套接字”来等待连接请求(就像一个插座等待插头插入)。当客户端发起连接时,会创建自己的“通信套接字”发出连接请求。服务端一旦 accept() 成功,就会从监听套接字中“派生出”一个新的“通信套接字”来与该客户端专线通信。因此,一个完整的 TCP 连接,至少包含:
-
客户端的通信套接字(插头)
-
服务端的监听套接字(插座面板)
-
服务端的通信套接字(插座上的孔)
-
监听套接字(lfd)只用于等待连接,不传输数据;
-
一旦连接建立,服务端与客户端各用一个通信套接字进行读写,彼此一对一独立传输;
-
服务器的监听套接字可以持续 accept,接更多连接;
-
每个客户端连接后,服务端就产生一个新的通信套接字,可由不同线程/进程处理。
这里附上一张经典的socket流程图,可能与我的代码不是完全对应,无所谓,能理解器大致流程就行
二、代码部分详解
其实这里还有很多代码我不是很了解,但也暂时无力去研究了,等后续再完整吧,现在只记录整体流程
2.1 服务端
#include <iostream>
#include <thread> // std::thread 线程库
#include <vector>
#include <string>
#include <algorithm> // std::transform 等
#include <cstring> // C 风格字符串操作
#include <memory>
#include <unistd.h> // POSIX 标准 API,如 read/write/close
#include <arpa/inet.h> // 网络地址转换函数
#include <netinet/in.h> // sockaddr_in 结构体
#define SERVER_PORT 9527 // 服务器端口号
#define BUFFER_SIZE 1024 // 缓冲区大小
// 处理客户端连接的函数(线程函数)
void handle_client(int client_fd, sockaddr_in client_addr) {
// 获取当前线程 ID
std::thread::id this_id = std::this_thread::get_id();
// 构造发送给客户端的欢迎信息(包含线程ID)
std::string thread_info = "Your handler thread ID is: " +
std::to_string(std::hash<std::thread::id>{}(this_id)) +
"\n";
// 向客户端发送线程ID
send(client_fd, thread_info.c_str(), thread_info.length(), 0);
char buffer[BUFFER_SIZE];
ssize_t n;
// 循环读取客户端数据,转换为大写后回送
while ((n = read(client_fd, buffer, sizeof(buffer))) > 0) {
// 将收到的数据全部转换为大写
for (int i = 0; i < n; ++i) {
buffer[i] = toupper(buffer[i]);
}
// 把转换后的数据发回客户端
send(client_fd, buffer, n, 0);
}
// 客户端断开连接后关闭 socket 并打印信息
close(client_fd);
std::cout << "Client disconnected (Thread " << this_id << ")" << std::endl;
}
int main() {
// 创建服务器 socket(TCP 类型)
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
perror("socket");
return 1;
}
// 准备服务器地址结构体
sockaddr_in server_addr{}, client_addr{};
socklen_t client_len = sizeof(client_addr);
server_addr.sin_family = AF_INET; // 使用 IPv4
server_addr.sin_port = htons(SERVER_PORT); // 设置端口(网络字节序)
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听本机所有IP地址
// 绑定服务器 socket 到指定IP和端口
if (bind(server_fd, (sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("bind");
return 1;
}
// 设置 socket 为监听状态
if (listen(server_fd, 128) < 0) {
perror("listen");
return 1;
}
std::cout << "Server listening on port " << SERVER_PORT << "..." << std::endl;
// 主循环:接收客户端连接并创建新线程处理
while (true) {
// 接收客户端连接请求(阻塞)
int client_fd = accept(server_fd, (sockaddr*)&client_addr, &client_len);
if (client_fd < 0) {
perror("accept");
continue;
}
// 打印客户端地址信息
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, ip, sizeof(ip));
std::cout << "Client connected from " << ip << ":" << ntohs(client_addr.sin_port) << std::endl;
// 创建新线程处理客户端连接,并将 client_fd 传递进去
std::thread client_thread(handle_client, client_fd, client_addr);
// 设置线程分离,结束时自动释放资源
client_thread.detach();
}
close(server_fd);
return 0;
}
2.2、客户端代码
#include <iostream>
#include <string>
#include <cstring> // C风格字符串处理函数
#include <unistd.h> // read, write, close
#include <arpa/inet.h> // inet_pton 等函数
#define SERVER_PORT 9527 // 服务端端口
#define BUFFER_SIZE 1024 // 缓冲区大小
int main() {
// 创建客户端 socket
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket");
return 1;
}
// 准备服务器地址
sockaddr_in server_addr{};
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr); // 本机
// 连接服务器
if (connect(sockfd, (sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("connect");
return 1;
}
char buffer[BUFFER_SIZE];
// 接收来自服务器的线程 ID 欢迎信息
ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
if (n > 0) {
buffer[n] = '\0';
std::cout << "Server: " << buffer;
}
// 进入消息交互循环
while (true) {
std::string input;
std::cout << "Enter message (q to quit): ";
std::getline(std::cin, input);
if (input == "q") break;
// 向服务器发送输入字符串
send(sockfd, input.c_str(), input.length(), 0);
// 读取服务器的响应
n = read(sockfd, buffer, sizeof(buffer) - 1);
if (n > 0) {
buffer[n] = '\0';
std::cout << "Server replied: " << buffer;
}
}
close(sockfd);
return 0;
}
2.3、在终端中如何调用
- 在终端进行编译
-g++ -std=c++11 -pthread -o server server.cpp g++ -std=c++11 -o client client.cpp
- 运行代码:任意打开一个终端,进入虚拟机对应的目录下,先打开一个服务端
./server
- 然后仿造上面的步骤打开多个窗口,运行
./client
即可进行多线程的调试。
参考自
https://www.yuque.com/boyhui/ukcpwf/ieg4t7k1d32hvbvb#u311z