三、Socket 编程 TCP

Socket 编程 TCP

一、TCP 编程整体认识

TCP 是面向连接的可靠传输协议。和 UDP 不同,UDP 可以直接 sendto/recvfrom 收发数据,而 TCP 通信之前必须先建立连接。

TCP 服务端基本流程:

socket() -> bind() -> listen() -> accept() -> read/write -> close()

TCP 客户端基本流程:

socket() -> connect() -> write/read -> close()

服务端中有两个非常重要的 socket:

_listenSock:监听 socket,只负责获取新连接; sockfd:通信 socket,由 accept 返回,负责和某个客户端通信。

accept() 是 TCP 服务端非常关键的接口。监听 socket 并不直接和客户端通信,它只是负责等待连接。真正与客户端通信的是 accept() 返回的新 socket。

客户端一般不需要显式 bind(),因为客户端不需要固定端口。当客户端调用 connect() 时,操作系统会自动为客户端分配本地 IP 和临时端口。


二、TCP 常用接口说明

1. socket

int socket(int domain, int type, int protocol);

TCP 使用:

socket(AF_INET, SOCK_STREAM, 0);

含义:

AF_INET:IPv4; SOCK_STREAM:面向字节流,也就是 TCP; 0:使用默认协议。

2. bind

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

服务器需要绑定固定 IP 和端口。否则客户端不知道连接哪里。

常见写法:

local.sin_family = AF_INET; local.sin_port = htons(port); local.sin_addr.s_addr = htonl(INADDR_ANY);

INADDR_ANY 表示绑定本机任意 IP。

3. listen

int listen(int sockfd, int backlog);

listen() 把 socket 设置为监听状态。只有调用 listen() 后,服务器才可以接收客户端连接。

4. accept

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

accept() 从监听队列中获取一个已经建立好的连接,并返回新的通信 socket。

5. connect

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

客户端通过 connect() 连接服务器。connect() 填的是服务器地址,不是自己的地址。

6. read/write

TCP 连接建立后,可以像读写文件一样读写 socket:

read(sockfd, buffer, sizeof(buffer)); write(sockfd, data.c_str(), data.size());

但是 TCP 是面向字节流的,不保证一次 write() 对应一次完整 read()。这就是后面要解决的粘包问题。


三、服务器版本演进

V1:单进程版本

主进程 accept() 一个连接后,直接调用 Service() 处理客户端。

优点是简单,适合理解 TCP 基本流程。

缺点是一次只能服务一个客户端。如果当前客户端不退出,服务器就无法继续处理其他客户端。

V2:多进程版本

父进程只负责 accept(),每来一个客户端,就创建子进程处理。

优点是多个客户端可以并发处理。

缺点是进程创建成本较高,并且需要处理子进程回收问题,否则会产生僵尸进程。

V3:多线程版本

主线程只负责 accept(),每来一个客户端,就创建一个线程处理。

优点是比多进程更轻量。

缺点是客户端太多时,线程数量会快速增加,服务器压力变大。

V4:线程池版本

提前创建固定数量的工作线程,主线程负责接收连接,然后把任务投递到线程池。

优点是避免频繁创建和销毁线程,也能控制并发数量。

缺点是如果每个连接都是长连接,一个连接会长期占用一个线程。更高并发场景还需要继续学习 select/poll/epoll。


公共代码

下面这些文件四个版本都可以共用。

Comm.hpp

cpp 复制代码
#pragma once

#include <sys/types.h>
#include <sys/socket.h>

enum
{
    Usage_Err = 1,
    Socket_Err,
    Bind_Err,
    Listen_Err,
    Connect_Err
};

// 把 sockaddr_in* 转成 sockaddr*
#define CONV(addr_ptr) ((struct sockaddr *)(addr_ptr))

nocopy.hpp

cpp 复制代码
#pragma once

class nocopy
{
public:
    nocopy() = default;
    ~nocopy() = default;

    nocopy(const nocopy &) = delete;
    const nocopy &operator=(const nocopy &) = delete;
};

Log.hpp

cpp 复制代码
#pragma once

#include <cstdio>
#include <ctime>
#include <cstdarg>

enum LogLevel
{
    Debug = 0,
    Info,
    Warning,
    Error,
    Fatal
};

class Log
{
public:
    void LogMessage(LogLevel level, const char *format, ...)
    {
        const char *levelString[] = {
            "Debug",
            "Info",
            "Warning",
            "Error",
            "Fatal"};

        char timeBuffer[64];
        time_t curr = time(nullptr);
        struct tm *tm = localtime(&curr);

        snprintf(timeBuffer, sizeof(timeBuffer),
                 "%04d-%02d-%02d %02d:%02d:%02d",
                 tm->tm_year + 1900,
                 tm->tm_mon + 1,
                 tm->tm_mday,
                 tm->tm_hour,
                 tm->tm_min,
                 tm->tm_sec);

        printf("[%s][%s] ", timeBuffer, levelString[level]);

        va_list args;
        va_start(args, format);
        vprintf(format, args);
        va_end(args);
    }
};

static Log lg;

InetAddr.hpp

cpp 复制代码
#pragma once

#include <string>
#include <cstdint>
#include <netinet/in.h>

class InetAddr
{
public:
    InetAddr();
    explicit InetAddr(const struct sockaddr_in &addr);

    std::string Ip() const;
    uint16_t Port() const;
    std::string ToString() const;

    const struct sockaddr_in &Addr() const;

private:
    std::string _ip;
    uint16_t _port;
    struct sockaddr_in _addr;
};

InetAddr.cc

cpp 复制代码
#include "InetAddr.hpp"

#include <cstring>
#include <arpa/inet.h>

InetAddr::InetAddr()
    : _ip("0.0.0.0"), _port(0)
{
    memset(&_addr, 0, sizeof(_addr));
}

InetAddr::InetAddr(const struct sockaddr_in &addr)
    : _addr(addr)
{
    char ipBuffer[64];

    // inet_ntop 是线程安全版本,比 inet_ntoa 更推荐
    inet_ntop(AF_INET, &_addr.sin_addr, ipBuffer, sizeof(ipBuffer));

    _ip = ipBuffer;
    _port = ntohs(_addr.sin_port);
}

std::string InetAddr::Ip() const
{
    return _ip;
}

uint16_t InetAddr::Port() const
{
    return _port;
}

std::string InetAddr::ToString() const
{
    return _ip + ":" + std::to_string(_port);
}

const struct sockaddr_in &InetAddr::Addr() const
{
    return _addr;
}

V1 单进程 Echo Server

TcpServer.hpp

cpp 复制代码
#pragma once

#include <cstdint>

#include "nocopy.hpp"
#include "InetAddr.hpp"

const static int defaultBacklog = 6;

class TcpServer : public nocopy
{
public:
    explicit TcpServer(uint16_t port);
    ~TcpServer();

    void Init();
    void Start();

private:
    void Service(int sockfd, InetAddr peer);

private:
    uint16_t _port;
    int _listenSock;
    bool _isRunning;
};

TcpServer.cc

cpp 复制代码
#include "TcpServer.hpp"

#include <iostream>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <sys/socket.h>
#include <netinet/in.h>

#include "Comm.hpp"
#include "Log.hpp"

TcpServer::TcpServer(uint16_t port)
    : _port(port), _listenSock(-1), _isRunning(false)
{
}

void TcpServer::Init()
{
    // 防止向已经关闭的连接写入时,进程被 SIGPIPE 杀掉
    signal(SIGPIPE, SIG_IGN);

    // 1. 创建监听 socket
    _listenSock = socket(AF_INET, SOCK_STREAM, 0);
    if (_listenSock < 0)
    {
        lg.LogMessage(Fatal, "socket error, errno: %d, %s\n", errno, strerror(errno));
        exit(Socket_Err);
    }

    // 2. 设置端口复用,方便服务器重启
    int opt = 1;
    setsockopt(_listenSock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    // 3. 填写服务器本地地址
    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);

    // 4. 绑定 IP 和端口
    if (bind(_listenSock, CONV(&local), sizeof(local)) < 0)
    {
        lg.LogMessage(Fatal, "bind error, errno: %d, %s\n", errno, strerror(errno));
        exit(Bind_Err);
    }

    // 5. 设置监听状态
    if (listen(_listenSock, defaultBacklog) < 0)
    {
        lg.LogMessage(Fatal, "listen error, errno: %d, %s\n", errno, strerror(errno));
        exit(Listen_Err);
    }

    lg.LogMessage(Info, "server init success, port: %d\n", _port);
}

void TcpServer::Start()
{
    _isRunning = true;

    while (_isRunning)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);

        // accept 返回的是通信 socket
        int sockfd = accept(_listenSock, CONV(&peer), &len);
        if (sockfd < 0)
        {
            lg.LogMessage(Warning, "accept error, errno: %d, %s\n", errno, strerror(errno));
            continue;
        }

        InetAddr addr(peer);
        lg.LogMessage(Info, "new connection: %s, sockfd: %d\n",
                      addr.ToString().c_str(), sockfd);

        // V1:直接在主进程中提供服务
        Service(sockfd, addr);

        close(sockfd);
    }
}

void TcpServer::Service(int sockfd, InetAddr peer)
{
    char buffer[1024];

    while (true)
    {
        ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);

        if (n > 0)
        {
            buffer[n] = '\0';

            std::cout << "client[" << peer.ToString() << "] say# "
                      << buffer << std::endl;

            std::string echo = "server echo# ";
            echo += buffer;

            write(sockfd, echo.c_str(), echo.size());
        }
        else if (n == 0)
        {
            lg.LogMessage(Info, "client quit: %s\n", peer.ToString().c_str());
            break;
        }
        else
        {
            lg.LogMessage(Error, "read error, errno: %d, %s\n", errno, strerror(errno));
            break;
        }
    }
}

TcpServer::~TcpServer()
{
    if (_listenSock >= 0)
    {
        close(_listenSock);
    }
}

V2 多进程 Echo Server

V2 的核心变化:accept() 新连接后,创建子进程处理客户端。

TcpServer.hpp

cpp 复制代码
#pragma once

#include <cstdint>

#include "nocopy.hpp"
#include "InetAddr.hpp"

const static int defaultBacklog = 6;

class TcpServer : public nocopy
{
public:
    explicit TcpServer(uint16_t port);
    ~TcpServer();

    void Init();
    void Start();

private:
    void Service(int sockfd, InetAddr peer);
    void ProcessConnection(int sockfd, const struct sockaddr_in &peer);

private:
    uint16_t _port;
    int _listenSock;
    bool _isRunning;
};

TcpServer.cc

cpp 复制代码
#include "TcpServer.hpp"

#include <iostream>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <netinet/in.h>

#include "Comm.hpp"
#include "Log.hpp"

TcpServer::TcpServer(uint16_t port)
    : _port(port), _listenSock(-1), _isRunning(false)
{
}

void TcpServer::Init()
{
    signal(SIGPIPE, SIG_IGN);

    _listenSock = socket(AF_INET, SOCK_STREAM, 0);
    if (_listenSock < 0)
    {
        lg.LogMessage(Fatal, "socket error, errno: %d, %s\n", errno, strerror(errno));
        exit(Socket_Err);
    }

    int opt = 1;
    setsockopt(_listenSock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    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)
    {
        lg.LogMessage(Fatal, "bind error, errno: %d, %s\n", errno, strerror(errno));
        exit(Bind_Err);
    }

    if (listen(_listenSock, defaultBacklog) < 0)
    {
        lg.LogMessage(Fatal, "listen error, errno: %d, %s\n", errno, strerror(errno));
        exit(Listen_Err);
    }

    lg.LogMessage(Info, "server init success, port: %d\n", _port);
}

void TcpServer::Start()
{
    _isRunning = true;

    while (_isRunning)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);

        int sockfd = accept(_listenSock, CONV(&peer), &len);
        if (sockfd < 0)
        {
            lg.LogMessage(Warning, "accept error, errno: %d, %s\n", errno, strerror(errno));
            continue;
        }

        ProcessConnection(sockfd, peer);
    }
}

void TcpServer::ProcessConnection(int sockfd, const struct sockaddr_in &peer)
{
    InetAddr addr(peer);

    pid_t id = fork();
    if (id < 0)
    {
        lg.LogMessage(Error, "fork error, errno: %d, %s\n", errno, strerror(errno));
        close(sockfd);
        return;
    }
    else if (id == 0)
    {
        // 子进程不需要监听 socket
        close(_listenSock);

        Service(sockfd, addr);

        close(sockfd);
        exit(0);
    }
    else
    {
        // 父进程不负责通信,关闭自己的通信 socket
        close(sockfd);

        // 非阻塞回收,避免僵尸进程堆积
        while (waitpid(-1, nullptr, WNOHANG) > 0)
        {
        }
    }
}

void TcpServer::Service(int sockfd, InetAddr peer)
{
    char buffer[1024];

    while (true)
    {
        ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);

        if (n > 0)
        {
            buffer[n] = '\0';

            std::cout << "client[" << peer.ToString() << "] say# "
                      << buffer << std::endl;

            std::string echo = "server echo# ";
            echo += buffer;

            write(sockfd, echo.c_str(), echo.size());
        }
        else if (n == 0)
        {
            lg.LogMessage(Info, "client quit: %s\n", peer.ToString().c_str());
            break;
        }
        else
        {
            lg.LogMessage(Error, "read error, errno: %d, %s\n", errno, strerror(errno));
            break;
        }
    }
}

TcpServer::~TcpServer()
{
    if (_listenSock >= 0)
    {
        close(_listenSock);
    }
}

V3 多线程 Echo Server

V3 的核心变化:每个客户端连接交给一个线程处理。

TcpServer.hpp

cpp 复制代码
#pragma once

#include <cstdint>
#include <pthread.h>

#include "nocopy.hpp"
#include "InetAddr.hpp"

const static int defaultBacklog = 6;

class TcpServer : public nocopy
{
public:
    explicit TcpServer(uint16_t port);
    ~TcpServer();

    void Init();
    void Start();

private:
    class ThreadData
    {
    public:
        ThreadData(TcpServer *server, int sockfd, const struct sockaddr_in &peer);

    public:
        TcpServer *_server;
        int _sockfd;
        InetAddr _peer;
    };

private:
    static void *ThreadRoutine(void *args);

    void Service(int sockfd, InetAddr peer);
    void ProcessConnection(int sockfd, const struct sockaddr_in &peer);

private:
    uint16_t _port;
    int _listenSock;
    bool _isRunning;
};

TcpServer.cc

cpp 复制代码
#include "TcpServer.hpp"

#include <iostream>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <sys/socket.h>
#include <netinet/in.h>

#include "Comm.hpp"
#include "Log.hpp"

TcpServer::ThreadData::ThreadData(TcpServer *server, int sockfd, const struct sockaddr_in &peer)
    : _server(server), _sockfd(sockfd), _peer(peer)
{
}

TcpServer::TcpServer(uint16_t port)
    : _port(port), _listenSock(-1), _isRunning(false)
{
}

void TcpServer::Init()
{
    signal(SIGPIPE, SIG_IGN);

    _listenSock = socket(AF_INET, SOCK_STREAM, 0);
    if (_listenSock < 0)
    {
        lg.LogMessage(Fatal, "socket error, errno: %d, %s\n", errno, strerror(errno));
        exit(Socket_Err);
    }

    int opt = 1;
    setsockopt(_listenSock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    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)
    {
        lg.LogMessage(Fatal, "bind error, errno: %d, %s\n", errno, strerror(errno));
        exit(Bind_Err);
    }

    if (listen(_listenSock, defaultBacklog) < 0)
    {
        lg.LogMessage(Fatal, "listen error, errno: %d, %s\n", errno, strerror(errno));
        exit(Listen_Err);
    }

    lg.LogMessage(Info, "server init success, port: %d\n", _port);
}

void TcpServer::Start()
{
    _isRunning = true;

    while (_isRunning)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);

        int sockfd = accept(_listenSock, CONV(&peer), &len);
        if (sockfd < 0)
        {
            lg.LogMessage(Warning, "accept error, errno: %d, %s\n", errno, strerror(errno));
            continue;
        }

        ProcessConnection(sockfd, peer);
    }
}

void TcpServer::ProcessConnection(int sockfd, const struct sockaddr_in &peer)
{
    pthread_t tid;

    ThreadData *td = new ThreadData(this, sockfd, peer);

    int n = pthread_create(&tid, nullptr, ThreadRoutine, td);
    if (n != 0)
    {
        lg.LogMessage(Error, "pthread_create error\n");
        close(sockfd);
        delete td;
    }
}

void *TcpServer::ThreadRoutine(void *args)
{
    pthread_detach(pthread_self());

    ThreadData *td = static_cast<ThreadData *>(args);

    td->_server->Service(td->_sockfd, td->_peer);

    close(td->_sockfd);
    delete td;

    return nullptr;
}

void TcpServer::Service(int sockfd, InetAddr peer)
{
    char buffer[1024];

    while (true)
    {
        ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);

        if (n > 0)
        {
            buffer[n] = '\0';

            std::cout << "client[" << peer.ToString() << "] say# "
                      << buffer << std::endl;

            std::string echo = "server echo# ";
            echo += buffer;

            write(sockfd, echo.c_str(), echo.size());
        }
        else if (n == 0)
        {
            lg.LogMessage(Info, "client quit: %s\n", peer.ToString().c_str());
            break;
        }
        else
        {
            lg.LogMessage(Error, "read error, errno: %d, %s\n", errno, strerror(errno));
            break;
        }
    }
}

TcpServer::~TcpServer()
{
    if (_listenSock >= 0)
    {
        close(_listenSock);
    }
}

V4 线程池 Echo Server

ThreadPool.hpp

cpp 复制代码
#pragma once

#include <queue>
#include <mutex>
#include <thread>
#include <vector>
#include <condition_variable>

template <class Task>
class ThreadPool
{
public:
    static ThreadPool<Task> *GetInstance()
    {
        static ThreadPool<Task> instance;
        return &instance;
    }

    void Start()
    {
        std::lock_guard<std::mutex> lock(_startMutex);

        if (_isRunning)
        {
            return;
        }

        _isRunning = true;

        for (int i = 0; i < _threadNum; ++i)
        {
            _threads.emplace_back([this]() {
                this->ThreadRun();
            });
        }

        for (auto &thread : _threads)
        {
            thread.detach();
        }
    }

    void Push(const Task &task)
    {
        {
            std::lock_guard<std::mutex> lock(_mutex);
            _tasks.push(task);
        }

        _cond.notify_one();
    }

private:
    ThreadPool(int threadNum = 5)
        : _threadNum(threadNum), _isRunning(false)
    {
    }

    ThreadPool(const ThreadPool &) = delete;
    ThreadPool &operator=(const ThreadPool &) = delete;

    void ThreadRun()
    {
        while (true)
        {
            Task task;

            {
                std::unique_lock<std::mutex> lock(_mutex);

                _cond.wait(lock, [this]() {
                    return !_tasks.empty();
                });

                task = _tasks.front();
                _tasks.pop();
            }

            task();
        }
    }

private:
    int _threadNum;
    bool _isRunning;

    std::vector<std::thread> _threads;
    std::queue<Task> _tasks;

    std::mutex _mutex;
    std::mutex _startMutex;
    std::condition_variable _cond;
};

TcpServer.hpp

cpp 复制代码
#pragma once

#include <cstdint>

#include "nocopy.hpp"
#include "InetAddr.hpp"

const static int defaultBacklog = 6;

class TcpServer : public nocopy
{
public:
    explicit TcpServer(uint16_t port);
    ~TcpServer();

    void Init();
    void Start();

private:
    void Service(int sockfd, InetAddr peer);
    void ProcessConnection(int sockfd, const struct sockaddr_in &peer);

private:
    uint16_t _port;
    int _listenSock;
    bool _isRunning;
};

TcpServer.cc

cpp 复制代码
#include "TcpServer.hpp"

#include <iostream>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <functional>
#include <sys/socket.h>
#include <netinet/in.h>

#include "Comm.hpp"
#include "Log.hpp"
#include "ThreadPool.hpp"

TcpServer::TcpServer(uint16_t port)
    : _port(port), _listenSock(-1), _isRunning(false)
{
}

void TcpServer::Init()
{
    signal(SIGPIPE, SIG_IGN);

    _listenSock = socket(AF_INET, SOCK_STREAM, 0);
    if (_listenSock < 0)
    {
        lg.LogMessage(Fatal, "socket error, errno: %d, %s\n", errno, strerror(errno));
        exit(Socket_Err);
    }

    int opt = 1;
    setsockopt(_listenSock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    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)
    {
        lg.LogMessage(Fatal, "bind error, errno: %d, %s\n", errno, strerror(errno));
        exit(Bind_Err);
    }

    if (listen(_listenSock, defaultBacklog) < 0)
    {
        lg.LogMessage(Fatal, "listen error, errno: %d, %s\n", errno, strerror(errno));
        exit(Listen_Err);
    }

    using task_t = std::function<void()>;
    ThreadPool<task_t>::GetInstance()->Start();

    lg.LogMessage(Info, "server init success, port: %d\n", _port);
}

void TcpServer::Start()
{
    _isRunning = true;

    while (_isRunning)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);

        int sockfd = accept(_listenSock, CONV(&peer), &len);
        if (sockfd < 0)
        {
            lg.LogMessage(Warning, "accept error, errno: %d, %s\n", errno, strerror(errno));
            continue;
        }

        ProcessConnection(sockfd, peer);
    }
}

void TcpServer::ProcessConnection(int sockfd, const struct sockaddr_in &peer)
{
    using task_t = std::function<void()>;

    InetAddr addr(peer);

    // 把连接封装成任务,交给线程池处理
    task_t task = std::bind(&TcpServer::Service, this, sockfd, addr);

    ThreadPool<task_t>::GetInstance()->Push(task);
}

void TcpServer::Service(int sockfd, InetAddr peer)
{
    char buffer[1024];

    while (true)
    {
        ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);

        if (n > 0)
        {
            buffer[n] = '\0';

            std::cout << "client[" << peer.ToString() << "] say# "
                      << buffer << std::endl;

            std::string echo = "server echo# ";
            echo += buffer;

            write(sockfd, echo.c_str(), echo.size());
        }
        else if (n == 0)
        {
            lg.LogMessage(Info, "client quit: %s\n", peer.ToString().c_str());
            break;
        }
        else
        {
            lg.LogMessage(Error, "read error, errno: %d, %s\n", errno, strerror(errno));
            break;
        }
    }

    close(sockfd);
}

TcpServer::~TcpServer()
{
    if (_listenSock >= 0)
    {
        close(_listenSock);
    }
}

客户端源码

四个版本都可以使用这个客户端测试。

TcpClient.cc

cpp 复制代码
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "Comm.hpp"

static void Usage(const std::string &process)
{
    std::cout << "Usage: " << process << " server_ip server_port" << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        return Usage_Err;
    }

    std::string serverIp = argv[1];
    uint16_t serverPort = std::stoi(argv[2]);

    // 1. 创建 socket
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        return Socket_Err;
    }

    // 2. 填写服务器地址
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));

    server.sin_family = AF_INET;
    server.sin_port = htons(serverPort);

    if (inet_pton(AF_INET, serverIp.c_str(), &server.sin_addr) <= 0)
    {
        std::cerr << "inet_pton error" << std::endl;
        close(sockfd);
        return Connect_Err;
    }

    // 3. 连接服务器
    // 客户端通常不需要显式 bind,connect 时系统会自动绑定本地端口
    if (connect(sockfd, CONV(&server), sizeof(server)) < 0)
    {
        std::cerr << "connect error" << std::endl;
        close(sockfd);
        return Connect_Err;
    }

    std::cout << "connect server success" << std::endl;

    // 4. 通信
    while (true)
    {
        std::string line;

        std::cout << "Please Enter# ";
        std::getline(std::cin, line);

        if (line == "quit" || line == "exit")
        {
            break;
        }

        ssize_t n = write(sockfd, line.c_str(), line.size());
        if (n <= 0)
        {
            std::cerr << "write error" << std::endl;
            break;
        }

        char buffer[1024];
        ssize_t m = read(sockfd, buffer, sizeof(buffer) - 1);

        if (m > 0)
        {
            buffer[m] = '\0';
            std::cout << buffer << std::endl;
        }
        else if (m == 0)
        {
            std::cout << "server close connection" << std::endl;
            break;
        }
        else
        {
            std::cerr << "read error" << std::endl;
            break;
        }
    }

    close(sockfd);
    return 0;
}

服务端启动入口

ServerMain.cc

cpp 复制代码
#include <iostream>
#include <cstdlib>

#include "TcpServer.hpp"
#include "Comm.hpp"

static void Usage(const std::string &process)
{
    std::cout << "Usage: " << process << " port" << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        return Usage_Err;
    }

    uint16_t port = std::stoi(argv[1]);

    TcpServer server(port);

    server.Init();
    server.Start();

    return 0;
}

Makefile

cpp 复制代码
.PHONY: all clean

CXX=g++
CXXFLAGS=-std=c++17 -Wall -Wextra -pthread

all: tcp_server tcp_client

tcp_server: ServerMain.cc TcpServer.cc InetAddr.cc
	$(CXX) $(CXXFLAGS) -o $@ $^

tcp_client: TcpClient.cc
	$(CXX) $(CXXFLAGS) -o $@ $^

clean:
	rm -f tcp_server tcp_client

运行测试

编译:

make

启动服务端:

./tcp_server 8080

启动客户端:

./tcp_client 127.0.0.1 8080

客户端输入:

hello

服务器返回:

server echo# hello

输入:

quit

客户端退出。


最后总结

TCP 编程的主线是连接。

服务端先创建监听 socket,然后绑定端口,接着调用 listen() 进入监听状态。之后通过 accept() 获取客户端连接。accept() 返回的新 socket 才是真正用于通信的 socket。

单进程版本适合理解流程;多进程版本可以支持并发;多线程版本比多进程更轻量;线程池版本进一步减少线程频繁创建销毁的开销。

不过这些版本目前都还是简单 Echo Server,还没有解决 TCP 粘包问题。因为 TCP 是字节流协议,不保留消息边界。真正写业务服务器时,还需要设计应用层协议,例如:

报文长度 + 报文内容

后面继续学习协议设计、序列化反序列化、线程池任务处理、IO 多路复用时,TCP 服务器的结构就会越来越完整

相关推荐
摇滚侠5 小时前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush46 小时前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5206 小时前
Linux 11 动态监控指令top
linux
网络研究院7 小时前
2026年网络安全
网络·安全·法律·法规·趋势·发展
酣大智7 小时前
ARP代理--工作原理
运维·网络·arp·arp代理
treesforest7 小时前
AI安全系统如何识别异常访问?IP风险识别正在成为关键能力
网络·人工智能·tcp/ip·安全·web安全
不会C语言的男孩7 小时前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言
shushangyun_7 小时前
2026年快消品B2B系统推荐:支持终端门店订货、促销政策自动化的工具?
java·运维·网络·数据库·人工智能·spring·自动化
古城小栈7 小时前
Unix 与 Linux 异同小叙
linux·服务器·unix
2601_961845158 小时前
粉笔行测题库|系统班|刷题
网络·百度·微信·微信公众平台·facebook·新浪微博