前言:
TCP(传输控制协议)是一种面向连接、可靠的流式传输协议,与 UDP 的无连接特性不同,它通过三次握手建立连接、四次挥手断开连接,提供数据确认、重传机制,保证数据有序且完整传输。本文将基于 TCP Socket 编程,实现一个支持多客户端连接的英译汉服务,并详细解析 TCP 核心 API 及不同并发处理方案。
一、TCP 通信基本流程与核心 API
1. 通信流程概览
- 服务器端:创建套接字 → 绑定地址端口 → 监听连接 → 接受连接 → 数据交互 → 关闭连接
- 客户端:创建套接字 → 连接服务器 → 数据交互 → 关闭连接
2. 核心 API 详解(sys/socket.h)
socket():创建套接字int socket(int domain, int type, int protocol);
- 作用:打开一个网络通信端口,返回文件描述符(类似文件操作的
open())。- 参数:
domain:协议族,IPv4 用AF_INET;type:套接字类型,TCP 用SOCK_STREAM(面向流);protocol:协议,默认填 0(自动匹配 type 对应的协议)。- 返回值:成功返回非负文件描述符,失败返回 - 1。
bind():绑定地址与端口(服务器)int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 作用:将套接字与特定 IP 和端口绑定,使服务器能被客户端找到。
- 参数:
sockfd:socket()返回的套接字描述符;addr:通用地址结构体(需转换为struct sockaddr_in具体设置 IPv4 信息);addrlen:地址结构体长度。- 关键设置:
struct sockaddr_in local; local.sin_family = AF_INET; // IPv4 local.sin_port = htons(9999); // 端口(主机字节序→网络字节序) local.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定所有本地IP- 返回值:成功返回 0,失败返回 - 1。
listen():监听连接(服务器)int listen(int sockfd, int backlog);
- 作用:将套接字设为监听状态,允许接收客户端连接。
- 参数:
backlog:最大等待连接队列长度(通常设 5~10)。- 返回值:成功返回 0,失败返回 - 1。
accept():接受连接(服务器)int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 作用:从监听队列中取出一个连接,创建新的套接字用于与客户端通信(原套接字继续监听)。
- 参数:
addr:传出参数,存储客户端地址信息;addrlen:传入传出参数,地址结构体长度。- 返回值:成功返回新套接字描述符(用于通信),失败返回 - 1。
connect():连接服务器(客户端)int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 作用:客户端向服务器发起连接(三次握手在此过程完成)。
- 参数:
addr为服务器的地址信息(IP + 端口)。- 返回值:成功返回 0,失败返回 - 1。
数据读写:
read()/write()
- TCP 是流式传输,可直接用文件读写函数:
ssize_t read(int fd, void *buf, size_t count); // 从套接字读数据 ssize_t write(int fd, const void *buf, size_t count); // 向套接字写数据
二、英译汉服务实现(单连接版本)
1. 功能设计
- 客户端发送英文单词,服务器返回对应的中文翻译;
- 支持 “quit” 退出连接。
2. 核心代码实现
辅助类:nocopy(禁止拷贝,避免套接字描述符重复释放)
// nocopy.hpp
#pragma once
class nocopy {
public:
nocopy() = default;
nocopy(const nocopy&) = delete; // 禁止拷贝构造
nocopy& operator=(const nocopy&) = delete; // 禁止赋值
~nocopy() = default;
};
服务器端:TcpServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "nocopy.hpp"
#define CONV(addr_ptr) ((struct sockaddr*)addr_ptr) // 地址转换宏
const int PORT = 9999;
const int BUFFER_SIZE = 1024;
// 英译汉字典(简化版)
std::string translate(const std::string& english) {
std::string chinese;
if (english == "hello") chinese = "你好";
else if (english == "world") chinese = "世界";
else if (english == "computer") chinese = "电脑";
else if (english == "program") chinese = "程序";
else chinese = "未知单词";
return chinese;
}
class TcpServer : public nocopy {
private:
int _listensock; // 监听套接字
bool _isrunning; // 运行状态
public:
TcpServer() : _isrunning(false) {}
// 初始化服务器:创建套接字→绑定→监听
void Init() {
// 1. 创建监听套接字
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0) {
std::cerr << "创建套接字失败: " << strerror(errno) << std::endl;
exit(1);
}
// 设置端口复用(避免服务器重启时端口占用)
int opt = 1;
setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 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 = htonl(INADDR_ANY);
if (bind(_listensock, CONV(&local), sizeof(local)) < 0) {
std::cerr << "绑定失败: " << strerror(errno) << std::endl;
close(_listensock);
exit(1);
}
// 3. 监听连接
if (listen(_listensock, 5) < 0) {
std::cerr << "监听失败: " << strerror(errno) << std::endl;
close(_listensock);
exit(1);
}
_isrunning = true;
std::cout << "服务器启动成功,监听端口 " << PORT << std::endl;
}
// 处理客户端通信:英译汉
void Service(int client_sock) {
char buffer[BUFFER_SIZE];
while (true) {
// 读取客户端发送的英文单词
ssize_t n = read(client_sock, buffer, BUFFER_SIZE - 1);
if (n > 0) {
buffer[n] = '\0';
std::cout << "客户端发送: " << buffer << std::endl;
// 若客户端发送"quit",断开连接
if (std::string(buffer) == "quit") {
std::cout << "客户端请求断开连接" << std::endl;
break;
}
// 翻译并返回结果
std::string chinese = translate(buffer);
write(client_sock, chinese.c_str(), chinese.size());
}
else if (n == 0) { // 客户端关闭连接
std::cout << "客户端已断开" << std::endl;
break;
}
else { // 读取出错
std::cerr << "读取失败: " << strerror(errno) << std::endl;
break;
}
}
close(client_sock); // 关闭通信套接字
}
// 启动服务器:循环接受连接
void Start() {
while (_isrunning) {
struct sockaddr_in peer; // 客户端地址
socklen_t peer_len = sizeof(peer);
// 接受连接(阻塞等待)
int client_sock = accept(_listensock, CONV(&peer), &peer_len);
if (client_sock < 0) {
std::cerr << "接受连接失败: " << strerror(errno) << std::endl;
continue;
}
std::cout << "新客户端连接: " << inet_ntoa(peer.sin_addr)
<< ":" << ntohs(peer.sin_port) << std::endl;
Service(client_sock); // 处理该客户端(单连接版本:一次处理一个)
}
close(_listensock); // 关闭监听套接字
}
};
服务器主函数:server.cc
#include "TcpServer.hpp"
int main() {
TcpServer server;
server.Init();
server.Start();
return 0;
}
客户端:client.cc
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
const int PORT = 9999;
const int BUFFER_SIZE = 1024;
int main(int argc, char* argv[]) {
if (argc != 2) {
std::cerr << "用法: " << argv[0] << " 服务器IP" << std::endl;
return 1;
}
// 1. 创建客户端套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
std::cerr << "创建套接字失败: " << strerror(errno) << std::endl;
return 1;
}
// 2. 连接服务器
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(PORT);
if (inet_pton(AF_INET, argv[1], &server.sin_addr) <= 0) { // IP字符串→二进制
std::cerr << "无效的IP地址" << std::endl;
close(sockfd);
return 1;
}
if (connect(sockfd, (struct sockaddr*)&server, sizeof(server)) < 0) {
std::cerr << "连接服务器失败: " << strerror(errno) << std::endl;
close(sockfd);
return 1;
}
// 3. 交互:发送英文,接收中文翻译
char buffer[BUFFER_SIZE];
while (true) {
std::cout << "请输入英文单词(输入quit退出): ";
std::string input;
std::getline(std::cin, input);
// 发送数据到服务器
write(sockfd, input.c_str(), input.size());
if (input == "quit") break;
// 接收翻译结果
ssize_t n = read(sockfd, buffer, BUFFER_SIZE - 1);
if (n > 0) {
buffer[n] = '\0';
std::cout << "翻译结果: " << buffer << std::endl;
}
}
close(sockfd);
return 0;
}
三、支持多客户端:并发处理方案
单连接版本一次只能处理一个客户端,实际应用中需支持多并发。以下是三种常见方案:
1. 多进程版本
- 原理:每接受一个客户端连接,创建子进程处理该客户端,父进程继续接受新连接。
- 关键代码:
void ProcessConnection(int client_sock, const struct sockaddr_in& peer) { pid_t pid = fork(); if (pid == 0) { // 子进程 close(_listensock); // 子进程不需要监听套接字 Service(client_sock); // 处理客户端 exit(0); // 处理完退出 } else if (pid > 0) { // 父进程 close(client_sock); // 父进程不需要通信套接字 // 回收子进程资源(避免僵尸进程) waitpid(pid, nullptr, WNOHANG); } } - 优缺点:简单实现,但进程创建开销大,适合连接数少的场景。
2. 多线程版本
- 原理:每接受一个客户端连接,创建线程处理该客户端,主线程继续接受新连接。
- 关键代码:
// 线程数据:通信套接字+客户端地址 struct ThreadData { int sockfd; struct sockaddr_in addr; }; // 线程处理函数 static void* ThreadHandler(void* arg) { pthread_detach(pthread_self()); // 分离线程,自动回收资源 ThreadData* data = (ThreadData*)arg; Service(data->sockfd); // 处理客户端 close(data->sockfd); delete data; return nullptr; } void ProcessConnection(int client_sock, const struct sockaddr_in& peer) { ThreadData* data = new ThreadData{client_sock, peer}; pthread_t tid; pthread_create(&tid, nullptr, ThreadHandler, data); // 创建线程 } - 优缺点:线程开销小于进程,但大量连接时线程创建销毁仍有开销。
3. 线程池版本
- 原理:预先创建一批线程,客户端连接到来时,将任务(处理逻辑)加入线程池队列,线程池中的线程异步处理。
- 关键代码:
// 线程池(简化版) template <typename Task> class ThreadPool { private: // 线程池实现(队列+互斥锁+条件变量) // ... public: void Push(const Task& task) { // 添加任务 // 加锁入队,唤醒线程 } }; // 服务器中使用线程池 void ProcessConnection(int client_sock, const struct sockaddr_in& peer) { // 绑定处理函数与参数 auto task = std::bind(&TcpServer::Service, this, client_sock); ThreadPool<decltype(task)>::GetInstance()->Push(task); // 任务入池 } - 优缺点:避免频繁创建销毁线程,适合高并发场景,是工业级常用方案。
四、编译与运行
# 编译服务器(以多线程版本为例)
g++ server.cc -o translator_server -lpthread
# 编译客户端
g++ client.cc -o translator_client
运行步骤
- 启动服务器:
./translator_server- 启动客户端(多终端可启动多个):
./translator_client 127.0.0.1- 客户端输入英文单词(如 “hello”),接收中文翻译;输入 “quit” 退出。
五、总结
本文通过实现一个英译汉服务,详细讲解了 TCP Socket 编程的核心流程与 API,并对比了单连接、多进程、多线程、线程池四种处理方案的优缺点。TCP 的可靠性使其适合需要确保数据完整传输的场景(如本文的翻译服务、文件传输等),而并发方案的选择需根据实际业务的连接量和性能需求决定。

1755

被折叠的 条评论
为什么被折叠?



