云计算百科
云计算领域专业知识百科平台

Liunx网络编程套接字:TCP

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}

文章目录

  • Liunx网络编程套接字:TCP
    • TCP Socket API 详解
    • 封装 TCP Socket 类
    • TCP 网络程序(单连接版本)
      • TCP通用服务器封装
      • TCP通用客户端与英译汉实现
      • TCP 网络程序(多进程版本)
    • TCP 网络程序(多线程版本)
    • TCP 协议通讯流程
    • TCP 与 UDP 对比

Liunx网络编程套接字:TCP

上一章我们了解网络基础和UDP使用Linux网络套接字进行编程,本章节网络接着上一章的内容来了解TCP使用Linux网络套接字进行编程

TCP Socket API 详解

#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
int listen(int sockfd, int backlog);
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

TCP编程与UDP不同,它是一种面向连接的协议,因此API的调用流程更复杂。

  • socket():创建通信端点,返回文件描述符。domain 传 AF_INET,type 传 SOCK_STREAM(表示面向流的传输协议),protocol 一般传0。
  • bind():服务器将套接字与固定的IP和端口绑定,以便客户端能确切找到服务器。成功返回0,失败返回-1。
  • listen():服务器声明该套接字处于监听状态,等待客户端连接。backlog 参数指定了处于等待连接队列的最大长度(一般设置为5),用于限制同时等待处理的连接数。
  • accept():服务器从处于监听状态的套接字中取出一个已经完成三次握手的连接。addr 是传出参数,保存了客户端IP和端口;addrlen 是传入传出参数(传入时告知缓冲区大小,返回时反映实际长度)。如果当前没有连接,服务器会在此处阻塞等待。
  • connect():客户端主动向服务器发起连接。addr 参数填入的是**对方(服务器)**的地址信息,这与 bind 填自己地址截然不同。
  • // TCP服务器初始化与连接建立核心逻辑(伪代码演示)
    int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in serv_addr;
    bzero(&serv_addr, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听任意本机IP
    serv_addr.sin_port = htons(9999);
    bind(listen_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
    listen(listen_sock, 5);

    // 循环处理客户端连接
    while (1) {
    struct sockaddr_in cli_addr;
    socklen_t cli_len = sizeof(cli_addr);
    // 阻塞等待客户端连接,返回新的套接字用于通信
    int connfd = accept(listen_sock, (struct sockaddr*)&cli_addr, &cli_len);
    if (connfd < 0) { perror("accept"); continue; }
    // 使用 connfd 读写数据,listen_sock 继续监听后续连接
    }

    封装 TCP Socket 类

    与UDP类似,针对TCP频繁的API调用,我们需要利用C++面向对象的思想进行封装(TcpSocket类),以减少代码冗余。该类内部维护一个文件描述符 fd_,并将 Socket()、Bind()、Listen()、Accept()、Connect()、Recv()、Send() 等方法整合到一个类中,实现了资源(文件描述符)的自动初始化和关闭,符合RAII设计思想。

    #pragma once
    #include <stdio.h>
    #include <string.h>
    #include <stdlib.h>
    #include <string>
    #include <cassert>
    #include <unistd.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <fcntl.h>

    typedef struct sockaddr sockaddr;
    typedef struct sockaddr_in sockaddr_in;

    // 宏定义:简化返回值判断
    #define CHECK_RET(exp) if (!(exp)) { return false; }

    class TcpSocket {
    public:
    TcpSocket() : fd_(–1) { }

    TcpSocket(int fd) : fd_(fd) { }

    bool Socket() {
    fd_ = socket(AF_INET, SOCK_STREAM, 0);
    if (fd_ < 0) {
    perror("socket");
    return false;
    }
    printf("open fd = %d\\n", fd_);
    return true;
    }

    bool Close() const {
    close(fd_);
    printf("close fd = %d\\n", fd_);
    return true;
    }

    bool Bind(const std::string& ip, uint16_t port) const {
    sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = inet_addr(ip.c_str());
    addr.sin_port = htons(port);
    int ret = bind(fd_, (sockaddr*)&addr, sizeof(addr));
    if (ret < 0) {
    perror("bind");
    return false;
    }
    return true;
    }

    bool Listen(int num) const {
    int ret = listen(fd_, num);
    if (ret < 0) {
    perror("listen");
    return false;
    }
    return true;
    }

    bool Accept(TcpSocket* peer, std::string* ip = NULL, uint16_t* port = NULL) const {
    sockaddr_in peer_addr;
    socklen_t len = sizeof(peer_addr);
    int new_sock = accept(fd_, (sockaddr*)&peer_addr, &len);
    if (new_sock < 0) {
    perror("accept");
    return false;
    }
    printf("accept fd = %d\\n", new_sock);
    peer->fd_ = new_sock;
    if (ip != NULL) {
    *ip = inet_ntoa(peer_addr.sin_addr);
    }
    if (port != NULL) {
    *port = ntohs(peer_addr.sin_port);
    }
    return true;
    }

    bool Recv(std::string* buf) const {
    buf->clear();
    char tmp[1024] = {0};
    // [注意!] 这里的读并不算很严谨,因为一次 recv 并不能保证把所有的数据都全部读完
    ssize_t read_size = recv(fd_, tmp, sizeof(tmp), 0);
    if (read_size < 0) {
    perror("recv");
    return false;
    }
    if (read_size == 0) {
    return false; // 读到 0 表示对端关闭了连接
    }
    buf->assign(tmp, read_size);
    return true;
    }

    bool Send(const std::string& buf) const {
    ssize_t write_size = send(fd_, buf.data(), buf.size(), 0);
    if (write_size < 0) {
    perror("send");
    return false;
    }
    return true;
    }

    bool Connect(const std::string& ip, uint16_t port) const {
    sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = inet_addr(ip.c_str());
    addr.sin_port = htons(port);
    int ret = connect(fd_, (sockaddr*)&addr, sizeof(addr));
    if (ret < 0) {
    perror("connect");
    return false;
    }
    return true;
    }

    int GetFd() const {
    return fd_;
    }

    private:
    int fd_;
    };

    注意:因为TCP是字节流传输,所以这里的Recv()可能会出现只读到一部分内容。正确的接受方法我们后续进行了解。

    TCP 网络程序(单连接版本)

    TCP通用服务器封装

    TCP 服务器的核心流程是: socket() -> bind() -> listen() -> 循环 accept() -> 循环 recv()/send()。以下通用服务器实现了一个最基本的单连接版本,即接受了一个客户端后,服务器进程就在内层循环中一直处理该客户端的请求,直到客户端断开。

    #pragma once
    #include <functional>
    #include "tcp_socket.hpp"

    typedef std::function<void(const std::string& req, std::string* resp)> Handler;

    class TcpServer {
    public:
    TcpServer(const std::string& ip, uint16_t port) : ip_(ip), port_(port) { }

    bool Start(Handler handler) {
    // 1. 创建 socket
    CHECK_RET(listen_sock_.Socket());
    // 2. 绑定端口号
    CHECK_RET(listen_sock_.Bind(ip_, port_));
    // 3. 进行监听
    CHECK_RET(listen_sock_.Listen(5));

    // 4. 进入事件循环
    for (;;) {
    // 5. 进行 accept
    TcpSocket new_sock;
    std::string ip;
    uint16_t port = 0;
    if (!listen_sock_.Accept(&new_sock, &ip, &port)) {
    continue;
    }
    printf("[client %s:%d] connect!\\n", ip.c_str(), port);

    // 6. 进行循环读写
    for (;;) {
    std::string req;
    // 7. 读取请求,读取失败则结束循环
    bool ret = new_sock.Recv(&req);
    if (!ret) {
    printf("[client %s:%d] disconnect!\\n", ip.c_str(), port);
    new_sock.Close();
    break;
    }
    // 8. 计算响应
    std::string resp;
    handler(req, &resp);
    // 9. 写回响应
    new_sock.Send(resp);
    }
    }
    return true;
    }

    private:
    TcpSocket listen_sock_;
    std::string ip_;
    uint16_t port_;
    };

    • 单连接版本的 TCP 服务器存在严重的并发缺陷。因为 accept 获取到新连接后,程序进入了内层 for(;;) 循环不断 recv。
    • 如果该客户端一直不发送数据也不关闭连接,此时执行流会阻塞在内层的 Recv 上。此时如果有第二个客户端发起连接请求,服务器将无法执行到外层的 accept,导致第二个客户端被挂起无法通信。
    • 需要引入多进程、多线程或 I/O 多路复用(如 epoll)技术,将“接受连接”和“处理业务”解耦。

    TCP通用客户端与英译汉实现

    客户端逻辑较为简单:socket() -> connect() -> 循环 send()/recv()。由于 TCP 是面向连接的,客户端不需要在启动时 bind 端口,内核会自动分配。

    tcp_client.hpp

    #pragma once
    #include "tcp_socket.hpp"

    class TcpClient {
    public:
    TcpClient(const std::string& ip, uint16_t port) : ip_(ip), port_(port) {
    sock_.Socket();
    }
    ~TcpClient() {
    sock_.Close();
    }
    bool Connect() {
    return sock_.Connect(ip_, port_);
    }
    bool Recv(std::string* buf) {
    return sock_.Recv(buf);
    }
    bool Send(const std::string& buf) {
    return sock_.Send(buf);
    }
    private:
    TcpSocket sock_;
    std::string ip_;
    uint16_t port_;
    };

    dict_server.cc

    #include "tcp_server.hpp"
    #include <unordered_map>
    #include <iostream>

    std::unordered_map<std::string, std::string> g_dict;

    void Translate(const std::string& req, std::string* resp) {
    auto it = g_dict.find(req);
    if (it == g_dict.end()) {
    *resp = "未查到!";
    return;
    }
    *resp = it->second;
    }

    int main(int argc, char* argv[]) {
    if (argc != 3) {
    printf("Usage ./dict_server [ip] [port]\\n");
    return 1;
    }
    g_dict.insert(std::make_pair("hello", "你好"));
    g_dict.insert(std::make_pair("world", "世界"));
    g_dict.insert(std::make_pair("c++", "最好的编程语言"));

    TcpServer server(argv[1], atoi(argv[2]));
    server.Start(Translate);
    return 0;
    }

    dict_client.cc

    #include "tcp_client.hpp"
    #include <iostream>

    int main(int argc, char* argv[]) {
    if (argc != 3) {
    printf("Usage ./dict_client [ip] [port]\\n");
    return 1;
    }
    TcpClient client(argv[1], atoi(argv[2]));
    bool ret = client.Connect();
    if (!ret) {
    return 1;
    }
    for (;;) {
    std::string word;
    std::cout << "请输入您要查的单词: ";
    std::cin >> word;
    if (!std::cin) {
    std::cout << "Good Bye" << std::endl;
    break;
    }
    client.Send(word);
    std::string result;
    client.Recv(&result);
    std::cout << word << " 意思是 " << result << std::endl;
    }
    return 0;
    }

    TCP 网络程序(多进程版本)

    为了支持多连接,可以在 accept() 成功返回后,调用 fork() 创建子进程。让子进程去执行内部的 recv/send 循环,而父进程继续去执行外层的 accept()。同时,为了防止子进程退出后变成僵尸进程 (Zombie Process),需要处理 SIGCHLD 信号。

    tcp_process_server.hpp

    #pragma once
    #include <functional>
    #include <signal.h>
    #include <sys/wait.h>
    #include <unistd.h>
    #include "tcp_socket.hpp"

    typedef std::function<void(const std::string& req, std::string* resp)> Handler;

    class TcpProcessServer {
    public:
    TcpProcessServer(const std::string& ip, uint16_t port) : ip_(ip), port_(port) {
    // 信号注册:忽略子进程退出信号,由系统自动回收,防止僵尸进程
    signal(SIGCHLD, SIG_IGN);
    }

    void ProcessConnect(const TcpSocket& new_sock, const std::string& ip, uint16_t port, Handler handler) {
    for (;;) {
    std::string req;
    bool ret = new_sock.Recv(&req);
    if (!ret) {
    printf("[client %s:%d] disconnect!\\n", ip.c_str(), port);
    new_sock.Close();
    break;
    }
    std::string resp;
    handler(req, &resp);
    new_sock.Send(resp);
    }
    }

    bool Start(Handler handler) {
    CHECK_RET(listen_sock_.Socket());
    CHECK_RET(listen_sock_.Bind(ip_, port_));
    CHECK_RET(listen_sock_.Listen(5));

    for (;;) {
    TcpSocket new_sock;
    std::string ip;
    uint16_t port = 0;
    if (!listen_sock_.Accept(&new_sock, &ip, &port)) {
    continue;
    }
    printf("[client %s:%d] connect!\\n", ip.c_str(), port);

    pid_t pid = fork();
    if (pid == 0) {
    // 子进程
    // 注意:子进程不需要监听套接字
    listen_sock_.Close();
    ProcessConnect(new_sock, ip, port, handler);
    // 子进程处理完毕,必须退出,否则会回到循环里继续 accept
    exit(0);
    } else if (pid > 0) {
    // 父进程
    // 注意:父进程不需要已建立连接的套接字
    new_sock.Close();
    } else {
    perror("fork");
    continue;
    }
    }
    }

    private:
    TcpSocket listen_sock_;
    std::string ip_;
    uint16_t port_;
    };

    • 子进程继承了父进程的 listen_sock_ 和父进程继承了 new_sock。
    • 文件描述符在进程间是共享的。如果父进程不关闭 new_sock,会导致连接的文件描述符引用计数无法归零,客户端断开后资源不释放;如果子进程不关闭 listen_sock_,如果父进程退出了,子进程仍然占用着监听端口,导致服务器无法重启。
    • 在子进程中 listen_sock_.Close();,在父进程中 new_sock.Close();。

    TCP 网络程序(多线程版本)

    多线程版本本质上与多进程版本类似,但是在同一个进程内利用线程处理连接。这需要用 pthread_create 创建线程,且传递给线程的函数必须是 static 的(因为普通成员函数带有隐含的 this 指针),并需要封装一个包含 new_sock、ip、port 和 handler 的结构体 ThreadArg 传入线程入口函数。同样,使用 pthread_detach 分离线程,让系统自动回收资源。

    tcp_thread_server.hpp

    #pragma once
    #include <functional>
    #include <pthread.h>
    #include "tcp_socket.hpp"

    typedef std::function<void(const std::string&, std::string*)> Handler;

    struct ThreadArg {
    TcpSocket new_sock;
    std::string ip;
    uint16_t port;
    Handler handler;
    };

    class TcpThreadServer {
    public:
    TcpThreadServer(const std::string& ip, uint16_t port) : ip_(ip), port_(port) { }

    bool Start(Handler handler) {
    CHECK_RET(listen_sock_.Socket());
    CHECK_RET(listen_sock_.Bind(ip_, port_));
    CHECK_RET(listen_sock_.Listen(5));

    for (;;) {
    ThreadArg* arg = new ThreadArg();
    arg->handler = handler;
    bool ret = listen_sock_.Accept(&arg->new_sock, &arg->ip, &arg->port);
    if (!ret) {
    delete arg; // Accept 失败,需清理内存
    continue;
    }
    printf("[client %s:%d] connect!\\n", arg->ip.c_str(), arg->port);

    // 6. 创建新的线程完成具体操作
    pthread_t tid;
    pthread_create(&tid, NULL, ThreadEntry, arg);
    // 线程分离,退出时自动回收资源
    pthread_detach(tid);
    }
    return true;
    }

    // 这里的成员函数必须是 static,因为 pthread_create 需要 void*(*)(void*)
    static void* ThreadEntry(void* arg) {
    ThreadArg* p = reinterpret_cast<ThreadArg*>(arg);
    ProcessConnect(p);
    // 一定要记得释放内存!!!也要记得关闭文件描述符
    p->new_sock.Close();
    delete p;
    return NULL;
    }

    // 处理单次连接。这个函数也得是 static,因为它不需要 this 指针
    static void ProcessConnect(ThreadArg* arg) {
    for (;;) {
    std::string req;
    bool ret = arg->new_sock.Recv(&req);
    if (!ret) {
    printf("[client %s:%d] disconnected!\\n", arg->ip.c_str(), arg->port);
    break;
    }
    std::string resp;
    arg->handler(req, &resp);
    arg->new_sock.Send(resp);
    printf("[client %s:%d] req: %s, resp: %s\\n", arg->ip.c_str(), arg->port, req.c_str(), resp.c_str());
    }
    }

    private:
    TcpSocket listen_sock_;
    std::string ip_;
    uint16_t port_;
    };

    TCP 协议通讯流程

    下图是基于TCP协议的客户端/服务器程序的一般流程: 在这里插入图片描述

  • 建立连接(三次握手):

    • 客户端调用 connect():TCP 协议层发出 SYN 段,客户端进入 SYN_SENT 状态。
    • 服务器收到 SYN:TCP 协议层返回 SYN+ACK 段,服务器进入 SYN_RCVD 状态。
    • 客户端收到 SYN+ACK:connect() 返回,同时 TCP 协议层发送 ACK 段,双方进入 ESTABLISHED 态。
  • 数据传输:

    • 连接建立后,TCP 提供全双工通信。客户端调用 write() / send() 发送数据,服务器从 read(), recv() 读数据。如果没有数据,read() 会阻塞等待。
  • 断开连接(四次挥手):

    • 客户端调用 close():TCP 协议层发送 FIN 段,进入 FIN_WAIT_1 状态。
    • 服务器收到 FIN:TCP 协议层返回 ACK 段,服务器进入 CLOSE_WAIT 状态。此时服务器会收到 read() 返回 0,告知应用层对端已关闭。
    • 服务器调用 close():TCP 协议层发送 FIN 段,服务器进入 LAST_ACK 状态。
    • 客户端收到 FIN:TCP 协议层返回 ACK 段,进入 TIME_WAIT 状态,等待 2MSL 后彻底关闭。
  • 如何理解TCP的三次握手和四次挥手。

    • 我们通过谈恋爱的例子来比喻一下:我们将客户端想象成谈恋爱中的男生,服务端想象成女生。谈恋爱我知道首先得有对象才能谈,男生找女生谈恋爱首先就是要找喜欢的女生进行表白,同时女生要同意了你们才能在一起谈恋爱。
    • 情感中总会遇到一下挫折和矛盾多了或者不解决就会导致双方走不下去分手了,四次挥手就像是男生问女生你到底爱不爱我,因为这个问题问的太多了女生就一气之下说不爱你并对后续你的信息进行已读不回,男生就想挽留这段关系毕竟谈了这么久,男生就自我安慰说再等一段时间如果还是已读不回就爱谁谁也不谈了。

    TCP 与 UDP 对比

    • TCP:面向连接的、可靠的、面向字节流的传输层协议。通过序列号、确认应答、超时重传、流量控制、拥塞控制等机制,保证数据无差错、不丢失、不重复且按序到达。适用于文件传输、网页浏览等场景。
    • UDP:无连接的、不可靠的、面向数据报的传输层协议。只负责把数据发送出去,不保证到达,也不保证顺序。开销极小,传输速度快,适用于视频通话、游戏、DNS 查询等实时性场景。

    完

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Liunx网络编程套接字:TCP
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!