TCP Socket 编程实战:实现简易英译汉服务

前言:

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 和端口绑定,使服务器能被客户端找到。
  • 参数
    • sockfdsocket()返回的套接字描述符;
    • 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

运行步骤

  1. 启动服务器:./translator_server
  2. 启动客户端(多终端可启动多个):./translator_client 127.0.0.1
  3. 客户端输入英文单词(如 “hello”),接收中文翻译;输入 “quit” 退出。

五、总结

本文通过实现一个英译汉服务,详细讲解了 TCP Socket 编程的核心流程与 API,并对比了单连接、多进程、多线程、线程池四种处理方案的优缺点。TCP 的可靠性使其适合需要确保数据完整传输的场景(如本文的翻译服务、文件传输等),而并发方案的选择需根据实际业务的连接量和性能需求决定。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值