TCP网络编程:从入门到精通

文章目录

  • [Echo server](#Echo server)
    • [1.1 功能介绍](#1.1 功能介绍)
    • [1.2 TCP核心接口](#1.2 TCP核心接口)
      • [1. socket():创建套接字(通信端点)](#1. socket():创建套接字(通信端点))
      • [2. bind():绑定地址和端口](#2. bind():绑定地址和端口)
      • [3. listen():监听连接(仅服务器端)](#3. listen():监听连接(仅服务器端))
      • [4. accept():接受连接(仅服务器端)](#4. accept():接受连接(仅服务器端))
      • [5. connect():发起连接(仅客户端)](#5. connect():发起连接(仅客户端))
      • [6. read()/write() / recv()/send():收发数据](#6. read()/write() / recv()/send():收发数据)
      • [7. close():关闭连接](#7. close():关闭连接)
      • [8. 总结](#8. 总结)
    • [1.3 demo1 v1](#1.3 demo1 v1)
    • [1.4 实验结果](#1.4 实验结果)
    • [1.5 流程详解](#1.5 流程详解)
      • [1.5.1 关于listen() 函数的核心作用](#1.5.1 关于listen() 函数的核心作用)
        • [1. 标准原型](#1. 标准原型)
        • [2. 参数详解](#2. 参数详解)
        • [3. 返回值](#3. 返回值)
        • [4. 关键注意事项](#4. 关键注意事项)
        • [5. listen() 调用后的套接字状态变化](#5. listen() 调用后的套接字状态变化)
        • [6. 总结](#6. 总结)
      • [1.5.2 accept() 函数的核心作用](#1.5.2 accept() 函数的核心作用)
        • [1. 标准原型](#1. 标准原型)
        • [2. 参数详解](#2. 参数详解)
        • [3. 返回值](#3. 返回值)
        • [4. 关键特性:阻塞/非阻塞行为](#4. 关键特性:阻塞/非阻塞行为)
        • [5. 常见错误与注意事项](#5. 常见错误与注意事项)
          • [1. 高频错误原因](#1. 高频错误原因)
          • [2. 重要注意事项](#2. 重要注意事项)
        • [6. 总结](#6. 总结)
      • [1.5.3 connect() 函数的核心作用](#1.5.3 connect() 函数的核心作用)
        • [1. 标准原型](#1. 标准原型)
        • [2. 参数详解](#2. 参数详解)
        • [3. 返回值](#3. 返回值)
        • [4. 关键特性:阻塞/非阻塞行为](#4. 关键特性:阻塞/非阻塞行为)
        • [5. 常见错误与注意事项](#5. 常见错误与注意事项)
          • [1. 高频错误原因](#1. 高频错误原因)
          • [2. 重要注意事项](#2. 重要注意事项)
        • [6. 总结](#6. 总结)
    • [demo1 v2](#demo1 v2)
    • [demo1 v3](#demo1 v3)
  • demo2

本文主要是关于UDP网络通信编程的两个demo,通过这两个demo完全掌握UDP网络通信。

Echo server

1.1 功能介绍

功能:简单的回显服务器和客戶端代码,客户端向服务器发送消息,服务器接受消息之后向客户端回显。

1.2 TCP核心接口

TCP 是面向连接的可靠传输协议,基于它的网络编程(以 Linux/Unix 环境为例)遵循经典的 C/S(客户端/服务器)模型,核心接口围绕"创建套接字-绑定地址-监听-接受连接-收发数据-关闭连接"这一流程展开。下面按使用顺序逐一介绍核心接口:

1. socket():创建套接字(通信端点)

这是所有网络编程的第一步,用于创建一个"套接字文件描述符",相当于为网络通信打开一个"通道"。

  • 函数原型

    c 复制代码
    #include <sys/socket.h>
    int socket(int domain, int type, int protocol);
  • 参数说明

    • domain:地址族,TCP 用 AF_INET(IPv4)或 AF_INET6(IPv6);
    • type:套接字类型,TCP 必须用 SOCK_STREAM(流式套接字,对应面向连接的可靠传输);
    • protocol:协议类型,TCP 填 0(系统会自动匹配 SOCK_STREAM 对应的 TCP 协议)。
  • 返回值 :成功返回非负的文件描述符,失败返回 -1

  • 示例

    c 复制代码
    // 创建TCP套接字(IPv4)
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("socket failed"); // 错误提示
        exit(1);
    }

2. bind():绑定地址和端口

将创建的套接字与服务器的 IP 地址、端口号绑定,让客户端能找到对应的服务器。

  • 函数原型

    c 复制代码
    int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 参数说明

    • sockfdsocket() 返回的套接字描述符;
    • addr:指向包含 IP 和端口的地址结构体(IPv4 用 struct sockaddr_in);
    • addrlen:地址结构体的长度。
  • 示例

    c 复制代码
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr)); // 初始化结构体
    server_addr.sin_family = AF_INET; // IPv4
    server_addr.sin_addr.s_addr = INADDR_ANY; // 绑定所有本机IP
    server_addr.sin_port = htons(8080); // 绑定8080端口(htons转换字节序)
    
    if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind failed");
        close(sockfd);
        exit(1);
    }

3. listen():监听连接(仅服务器端)

将绑定后的套接字转为"监听套接字",开始等待客户端的连接请求。

  • 函数原型

    c 复制代码
    int listen(int sockfd, int backlog);
  • 参数说明

    • sockfd:绑定后的套接字描述符;
    • backlog:未处理的连接请求队列的最大长度(如 5,现代系统会自动调整)。
  • 示例

    c 复制代码
    if (listen(sockfd, 5) == -1) {
        perror("listen failed");
        close(sockfd);
        exit(1);
    }
    printf("Server listening on port 8080...\n");

4. accept():接受连接(仅服务器端)

从监听队列中取出一个已完成的客户端连接,返回一个新的套接字描述符(专门用于和该客户端通信),原监听套接字仍继续监听其他连接。

  • 函数原型

    c 复制代码
    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • 参数说明

    • sockfd:监听套接字描述符;
    • addr:输出参数,存储客户端的 IP 和端口信息(可填 NULL 表示不关心);
    • addrlen:输入输出参数,传入 addr 结构体的长度,返回实际使用的长度。
  • 示例

    c 复制代码
    struct sockaddr_in client_addr;
    socklen_t client_len = sizeof(client_addr);
    // 阻塞等待客户端连接
    int connfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len);
    if (connfd == -1) {
        perror("accept failed");
        close(sockfd);
        exit(1);
    }
    // 打印客户端信息
    char client_ip[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN);
    printf("Client connected: %s:%d\n", client_ip, ntohs(client_addr.sin_port));

5. connect():发起连接(仅客户端)

客户端通过该接口向服务器发起 TCP 连接请求,完成三次握手。

  • 函数原型

    c 复制代码
    int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 参数说明

    • sockfd:客户端创建的套接字描述符;
    • addr:服务器的 IP 和端口地址结构体;
    • addrlen:地址结构体长度。
  • 示例

    c 复制代码
    int client_fd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080);
    // 将服务器IP(如127.0.0.1)转为网络字节序
    inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
    
    if (connect(client_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("connect failed");
        close(client_fd);
        exit(1);
    }
    printf("Connected to server successfully!\n");

6. read()/write() / recv()/send():收发数据

连接建立后,服务器和客户端通过 accept()/connect() 返回的套接字描述符收发数据:

  • read()/write():通用的文件读写接口,可直接用于套接字(因为套接字是文件描述符);

  • recv()/send():专门的网络数据收发接口,支持额外参数(如 MSG_NOSIGNAL 避免断连触发信号)。

  • 示例(服务器接收+发送)

    c 复制代码
    char buf[1024];
    // 接收客户端数据
    ssize_t n = read(connfd, buf, sizeof(buf)-1);
    if (n <= 0) {
        perror("read failed");
        close(connfd);
        return;
    }
    buf[n] = '\0'; // 加字符串结束符
    printf("Received from client: %s\n", buf);
    
    // 向客户端发送响应
    const char *resp = "Hello from server!";
    write(connfd, resp, strlen(resp));

7. close():关闭连接

释放套接字描述符,终止 TCP 连接(会触发四次挥手)。

  • 函数原型

    c 复制代码
    int close(int fd);
  • 示例

    c 复制代码
    close(connfd); // 关闭和客户端的通信套接字
    close(sockfd); // 关闭服务器的监听套接字

8. 总结

  1. TCP 编程核心流程:服务器 socket() → bind() → listen() → accept() → 收发数据 → close();客户端 socket() → connect() → 收发数据 → close()
  2. 关键区分:listen() 仅标记套接字为"监听状态",accept() 才真正接收连接并返回新的通信套接字(原监听套接字可复用)。
  3. 核心接口的作用:socket 创建通道,bind 绑定地址,listen 开启监听,accept/connect 建立连接,read/write 传输数据,close 释放资源。

1.3 demo1 v1

v1版本仅能实现单进程连接访问服务器,仅仅是作为框架演示。
server.hpp

cpp 复制代码
#pragma once
#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"

using namespace LogModule;
const static int defaultsockfd = -1;
const static int backlog = 8;
class TcpServer : public NoCopy
{
public:
    TcpServer(uint16_t port) : _port(port), _listensockfd(defaultsockfd), _isrunning(false)
    {
    }

    void Init()
    {
        // 1. 创建套接字,socket类比open
        _listensockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket error";
            exit(SOCKET_ERR);
        }
        LOG(LogLevel ::INFO) << "socket success: " << _listensockfd;

        // 2. bind端口号
        InetAddr local(_port);
        int n = bind(_listensockfd, local.NetAddrPtr(), local.NetAddrLen());
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bing error";
            exit(BIND_ERR);
        }
        LOG(LogLevel ::INFO) << "bind success: " << _listensockfd;

        // 3. 设置socket状态
        n = listen(_listensockfd, backlog);
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "listen error";
            exit(LISTEN_ERR);
        }
        LOG(LogLevel ::INFO) << "listen success: " << _listensockfd;
    }
    void Service(int sockfd, InetAddr &peer)
    {
        char buffer[1024];
        while (true)
        {
            // 1. 先读取数据
            ssize_t n = read(sockfd, buffer, sizeof(buffer - 1));
            if (n > 0)
            {
                // 给buffer设置成C风格的字符串
                buffer[n] = 0;
                LOG(LogLevel::DEBUG) << peer.StringAddr() << "say# " << buffer;
                // 2. 写回数据
                std::string echo_string = buffer;
                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            else if (n == 0)
            {
                LOG(LogLevel::DEBUG) << peer.StringAddr() << "退出了...";
                close(sockfd);
                break;
            }
            else
            {
                LOG(LogLevel::DEBUG) << peer.StringAddr() << "异常...";
                close(sockfd);
                break;
            }
        }
    }

    void Run()
    {
        _isrunning = true;
        while (_isrunning)
        {
            // 获取链接 :其实内核已经建立连接了,获取链接得意义是让程序员知道服务器被谁链接,方便通信
            struct sockaddr_in peer;
            socklen_t len = sizeof(sockaddr_in);
            int sockfd = accept(_listensockfd, CONV(peer), &len);
            if (sockfd < 0)
            {
                LOG(LogLevel::WARNING) << "accept error";
                continue;
            }
            InetAddr addr(peer);
            LOG(LogLevel ::INFO) << "accept success, peer addr: " << addr.StringAddr();

            Service(sockfd, addr);
        }
        _isrunning = false;
    }

    ~TcpServer() {}

private:
    uint16_t _port;
    int _listensockfd;
    bool _isrunning;
};

server.cc

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

void Usage(std::string proc)
{
    std::cerr << "Usage: " << proc << "port" << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = std::stoi(argv[1]);
    Enable_Console_Log_Strategy();

    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port);
    tsvr->Init();
    tsvr->Run();

    return 0;
}

client.hpp

cpp 复制代码
#include <iostream>
#include "Common.hpp"
#include "InetAddr.hpp"

        
void Usage(std::string proc)
{
    std::cerr << "Usage: " << proc << " server_ip server_port" << std::endl;
}

// ./tcpclient server_ip server_port
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(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;
        exit(SOCKET_ERR);
    }

    // 2. 发起连接
    InetAddr serveraddr(serverip, serverport);
    int n = connect(sockfd, serveraddr.NetAddrPtr(), serveraddr.NetAddrLen());
    if(n < 0)
    {
        std::cerr << "connect error" << std::endl;
        exit(CONNECT_ERR);
    }

    // 3. echo client
    while(true)
    {
        std::string line;
        std::cout << "Please Enter@ ";
        std::getline(std::cin, line);

        write(sockfd, line.c_str(), line.size());

        char buffer[1024];
        ssize_t size = read(sockfd, buffer, sizeof(buffer)-1);
        if(size > 0)
        {
            buffer[size] = 0;
            std::cout << "server echo# " << buffer << std::endl;
        }
    }

    close(sockfd);
    return 0;
}

InetAddr.hpp

cpp 复制代码
#pragma once
#include "Common.hpp"
// 网络地址和主机地址之间进行转换的类

class InetAddr
{
public:
    InetAddr() {}
    InetAddr(struct sockaddr_in &addr) : _addr(addr)
    {
        _port = ntohs(_addr.sin_port);
        char ipbuffer[64];
        inet_ntop(AF_INET, &_addr.sin_addr, ipbuffer, sizeof(_addr));
        _ip = ipbuffer;
    }
    InetAddr(const std::string &ip, uint16_t port) : _ip(ip), _port(port)
    {
        memset(&_addr, 0, sizeof(_addr));
        _addr.sin_family = AF_INET;
        inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);
        _addr.sin_port = htons(_port);
    }
    InetAddr(uint16_t port) : _port(port), _ip()
    {
        memset(&_addr, 0, sizeof(_addr));
        _addr.sin_family = AF_INET;
        _addr.sin_addr.s_addr = INADDR_ANY;
        _addr.sin_port = htons(_port);
    }
    uint16_t Port() { return _port; }
    std::string Ip() { return _ip; }
    const struct sockaddr_in &NetAddr() { return _addr; }
    const struct sockaddr *NetAddrPtr()
    {
        return CONV(_addr);
    }
    socklen_t NetAddrLen()
    {
        return sizeof(_addr);
    }
    bool operator==(const InetAddr &addr)
    {
        return addr._ip == _ip && addr._port == _port;
    }
    std::string StringAddr()
    {
        return _ip + ":" + std::to_string(_port);
    }
    ~InetAddr()
    {
    }

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

1.4 实验结果

1.5 流程详解

先看服务端代码

先判断运行服务器方式是否正确。

使用智能指针创建服务器对象,传参数就是为服务器指定的端口。

执行初始化方法。

先搂一眼服务器的成员变量,包括端口号,监听文件描述符,运行状态。

ok紧着执行初始化方法

首先创建套接字,这里的第二个参数变成了SOCK_STREAM表示流式套接字,是TCP专用。

判断是否创建成功,

输出日志信息。

这里还封装了common类,用来表示退出信息。

手动绑定端口号,绑定之前要先填写socket信息,这里专门封装了InetAddr类完成。

关于InetAddr类:

三个参数就是socket信息

重载的四个构造函数,这里用的最后一个:

清空结构体,填写家族协议,IP,端口号。

对于IP不需要手动绑定,服务器可能有多个网卡(有线/无线/内网/外网IP),绑特定IP只会响应该网卡的请求,其他网卡的请求会被丢弃;绑INADDR_ANY能接收所有网卡的请求。

对于端口号,需要将本地主机序列转化为网络序列。


这里对强转做了包装,方便bind函数的第二三参数的填写。

判断是否绑定成功,用日志输出。

以上和UDP其实都差不多...

TCP的特点是面向连接,就由这里的listen来体现。

1.5.1 关于listen() 函数的核心作用

listen() 是专属于TCP 服务器端 的系统调用,它的核心作用是:

将一个已经绑定(bind())了 IP 和端口的套接字(sockfd),从"主动套接字"转为"被动监听套接字",并告诉操作系统内核:该套接字准备好接受客户端的连接请求了

简单来说,listen() 是服务器"开启监听"的关键一步,调用后服务器才会真正开始等待客户端的连接,没有这一步,accept() 调用会直接失败。

一句话 listen() 的核心意义就是明确告诉操作系统:这个服务器套接字已经完成了绑定(IP + 端口),现在正式 "就绪",可以开始接收客户端的连接请求了。

1. 标准原型
c 复制代码
#include <sys/socket.h>
int listen(int sockfd, int backlog);
2. 参数详解
参数 含义与注意事项
sockfd 输入参数,必须是已经通过 socket() 创建、且通过 bind() 绑定了地址的 TCP 套接字描述符(SOCK_STREAM 类型)。
backlog 输入参数,核心是设置"未完成连接队列"+"已完成连接队列"的总长度上限(不同系统实现略有差异),具体见下文"工作原理"。
3. 返回值
  • 成功:返回 0
  • 失败:返回 -1,并设置全局变量 errno 表示错误原因(可通过 perror() 打印)。
4. 关键注意事项
  • listen() 仅"开启监听",不会阻塞 :调用后函数立即返回,不会等待客户端连接(阻塞发生在 accept() 阶段);
  • listen() 不处理连接,只准备接收:真正取出连接的是 accept()listen() 只是"搭好架子";
  • UDP 协议不支持 listen():UDP 是无连接协议,不需要监听,调用 listen() 会返回错误(EOPNOTSUPP)。
5. listen() 调用后的套接字状态变化

通过 netstat 命令可以直观看到变化:

  • 调用 bind() 后、listen() 前:套接字状态为 CLOSESYN_SENT(无监听);
  • 调用 listen() 后:套接字状态变为 LISTEN,表示已进入监听状态。

示例命令(查看 8080 端口):

bash 复制代码
netstat -anp | grep 8080
# 输出示例(LISTEN 状态表示监听成功):
# tcp        0      0 0.0.0.0:8080            0.0.0.0:*               LISTEN      12345/./server
6. 总结
  1. listen() 的核心是将绑定后的 TCP 套接字转为监听状态,让内核为其维护连接队列,是服务器接收连接的前提;
  2. backlog 参数主要控制已完成连接队列的长度,现代系统会自动调整,常用值 5/10/128 即可;
  3. 关键特性:listen() 非阻塞、仅用于 TCP 服务器、调用后套接字状态变为 LISTEN,且必须在 bind() 之后、accept() 之前调用。

初始化完事儿后运行服务器。

修改运行状态,创建socket_in结构体 peer(输出型的参数,目的是带回申请连接的客户端)

调用 accept方法,这里会阻塞等待客户端申请连接。

关于accept

1.5.2 accept() 函数的核心作用

accept() 是专属于 TCP 服务器端 的系统调用,它的核心作用是:

从监听套接字(listen() 后的套接字)的"已完成连接队列"中,取出第一个完成三次握手的客户端连接,创建并返回一个全新的"通信套接字" ------这个新套接字专门用于和该客户端收发数据,而原监听套接字会继续保持 LISTEN 状态,等待其他客户端的连接。

简单来说,accept() 是服务器"接起电话"的动作:监听套接字是"总机"(一直等来电),accept() 是总机接线员,接起一个来电后,会分机到一个新的"通信套接字"(专门和这个客户通话),总机继续等下一个来电。

1. 标准原型
c 复制代码
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
2. 参数详解
参数 类型 含义与使用注意事项
sockfd 输入参数 必须是已调用 socket()→bind()→listen() 的监听套接字描述符(状态为 LISTEN)。
addr 输出参数 指向 struct sockaddr 类型的指针,用于存储连接成功的客户端的 IP 地址和端口信息 ;若不关心客户端信息,可传 NULL
addrlen 输入/输出 指向 socklen_t 变量的指针: ① 调用前:赋值为 addr 结构体的长度(如 sizeof(struct sockaddr_in)); ② 调用后:内核会修改为实际存储的客户端地址长度。 若 addrNULL,此参数也传 NULL
3. 返回值
  • 成功 :返回一个非负的新套接字描述符 (通信套接字,状态为 ESTABLISHED);
  • 失败 :返回 -1,并设置全局变量 errno 表示错误原因(可通过 perror() 打印)。
4. 关键特性:阻塞/非阻塞行为

accept() 的阻塞性由监听套接字的类型(阻塞/非阻塞)决定(默认是阻塞套接字):

阻塞模式(默认)
  • 若"已完成连接队列"为空,accept()暂停程序执行(阻塞),直到队列中有新连接;
  • 这是最常用的模式,适合简单的单客户端服务器。
非阻塞模式
  • 需先通过 fcntl() 将监听套接字设为非阻塞:

    c 复制代码
    #include <fcntl.h>
    // 将listen_fd设为非阻塞
    int flags = fcntl(listen_fd, F_GETFL, 0);
    fcntl(listen_fd, F_SETFL, flags | O_NONBLOCK);
  • 若"已完成连接队列"为空,accept()立即返回 -1 ,并设置 errno = EAGAINEWOULDBLOCK

  • 适合高并发服务器(结合 select/epoll 实现多路复用)。

5. 常见错误与注意事项
1. 高频错误原因
errno 错误码 含义 常见场景
EINVAL 参数无效 监听套接字未调用 listen() 就调用 accept()
EBADF 文件描述符无效 sockfd 不是合法的套接字描述符;
EAGAIN/EWOULDBLOCK 非阻塞模式下无连接 非阻塞 accept() 调用时,已完成队列为空;
EINTR 调用被信号中断 阻塞 accept() 时,程序收到信号(如 Ctrl+C);
2. 重要注意事项
  • 两个套接字的区分accept() 返回的 conn_fd 是通信套接字,原 listen_fd 是监听套接字,二者独立,关闭 conn_fd 不影响 listen_fd
  • 监听套接字状态不变accept() 成功后,listen_fd 仍为 LISTEN 状态,可继续调用 accept() 接收新客户端;
  • 客户端信息的字节序client_addr.sin_addr(IP)和 client_addr.sin_port(端口)都是网络字节序 ,需用 inet_ntop()/ntohs() 转为主机字节序的字符串/数字。
6. 总结
  1. accept() 的核心是从已完成连接队列取出连接,创建新的通信套接字 ,原监听套接字保持 LISTEN 状态;
  2. 参数中 addr/addrlen 是输出参数,用于获取客户端信息,不关心可传 NULL
  3. 阻塞性由监听套接字类型决定:默认阻塞(等连接),非阻塞模式下无连接会立即返回错误;
  4. 关键区分:三次握手由内核自动完成,accept() 仅负责"取出"已建立的连接,不参与握手过程。

emmm...我的理解就是:调用accept本质上就是创建了一个新的套接字并且新套接字的状态是已经连接,作用就是和客户端进行通信,并且原先监听套接字并不改变,监听套接字的作用就只是监听和维护已经建立连接的队列。所以这里也就能理解为啥服务端类的成员变量是监听套接字。


ok,这里理一下思路

首先更改服务器运行状态,创建peer结构体(作用是带回连接的套接字),算一下peer类型的大小(等下特定服务里面有用),创建新的套接字描述符(文件描述符),调用accept函数(参数含义:和那个监听套接字里的队列连接?强制类型转化一下带回的参数,长度),判断是否连接成功(如果没有则跳过本次连接),(连接成功)创建InetAddr对象(用于输出日志信息),输出日志信息(让服务器打印日志,知道和谁建立了连接),调用指定服务(参数:服务器和客户端套接字信息)。

调用到service。明确我们的目标是接收客户端传来的信息,然后再回显。

创建缓冲区,读取数据(这里用的就是read函数,因为是面向字节流的)。关于read函数的参数(从哪读,读到哪,读的大小)

接着判断是否读取成功。然后用日志格式化一下要输出的信息,最后用write函数输出(依旧是因为write函数是面向字节流的)参数的意义:写到哪,写什么,写多少。

这里服务端就设计完了,接着看客户端:

指定调用格式,调用的时候要说清楚服务器的IP和端口。

创建客户端的sockfd,判断是否创建成功。

创建InetAddr对象,方便做格式的转化。

调用connect方法

关于connect方法

1.5.3 connect() 函数的核心作用

connect() 是专属于 TCP 客户端 的系统调用,它的核心作用是:

让客户端套接字主动向服务器端的监听套接字(LISTEN 状态)发起 TCP 连接请求,触发并完成三次握手,最终建立客户端与服务器之间的双向通信链路。

简单来说,connect() 是客户端"主动打电话给服务器"的动作------服务器的监听套接字是"总机",客户端通过 connect() 拨打这个总机号码,完成三次握手后,服务器的 accept() 会"接起电话"并分配通信套接字,双方就能开始对话。

1. 标准原型
c 复制代码
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
2. 参数详解
参数 类型 含义与使用注意事项
sockfd 输入参数 客户端通过 socket() 创建的 TCP 套接字描述符(SOCK_STREAM 类型),无需提前 bind()(内核会自动分配临时端口)。
addr 输入参数 指向 struct sockaddr 类型的指针,存储服务器的 IP 地址和端口信息 (必须是服务器已绑定、且处于 LISTEN 状态的地址)。
addrlen 输入参数 addr 结构体的长度(如 sizeof(struct sockaddr_in))。
3. 返回值
  • 成功 :返回 0,表示三次握手完成,客户端套接字进入 ESTABLISHED(已建立)状态;
  • 失败 :返回 -1,并设置全局变量 errno 表示错误原因(可通过 perror() 打印)。

关键细节:

  1. connect() 的阻塞/返回时机:默认情况下,connect() 会阻塞到三次握手完全完成(客户端发送 ACK 后)才返回 0;
  2. 客户端无需提前 bind():调用 socket() 后可直接 connect(),内核会自动为客户端分配本机 IP 和临时端口(如 54321);
  3. 三次握手是内核自动完成的:connect() 只是触发这个流程,无需开发者干预。
4. 关键特性:阻塞/非阻塞行为

connect() 的阻塞性由客户端套接字的类型(阻塞/非阻塞)决定(默认是阻塞套接字):

阻塞模式(默认)
  • 调用后阻塞,直到三次握手完成(返回 0)或超时/失败(返回 -1);
  • 超时时间由内核决定(Linux 下约 75 秒),超时后 errno 设为 ETIMEDOUT
  • 这是最常用的模式,适合简单客户端。
非阻塞模式
  • 需先将客户端套接字设为非阻塞:

    c 复制代码
    #include <fcntl.h>
    int flags = fcntl(client_fd, F_GETFL, 0);
    fcntl(client_fd, F_SETFL, flags | O_NONBLOCK);
  • 调用 connect() 后会立即返回 -1 ,但 errno 设为 EINPROGRESS(表示三次握手正在进行中);

  • 需通过 select()/epoll() 监听套接字的"可写事件",来判断三次握手是否完成;

  • 适合高并发客户端(如同时连接多个服务器)。

5. 常见错误与注意事项
1. 高频错误原因
errno 错误码 含义 常见场景
ECONNREFUSED 连接被拒绝 服务器未启动、端口错误,或服务器未调用 listen()
ETIMEDOUT 连接超时 服务器不可达(如网络不通),或三次握手超时;
EINPROGRESS 连接正在进行中 非阻塞模式下 connect() 立即返回的正常状态;
EADDRINUSE 地址已被使用 客户端手动 bind() 了已占用的端口;
EHOSTUNREACH 主机不可达 服务器 IP 错误,或路由不通;
2. 重要注意事项
  • 客户端无需 bind() :除非需要固定客户端端口,否则直接 socket()→connect() 即可,内核会自动分配临时端口;
  • 连接失败后需重新创建套接字 :若 connect() 失败(如超时),该套接字无法再次用于 connect(),需关闭后重新 socket()
  • UDP 不支持 connect()(但可调用) :UDP 是无连接协议,调用 connect() 仅会记录服务器地址,不会建立连接,后续 send() 无需指定地址。
6. 总结
  1. connect() 是客户端主动发起 TCP 连接的核心接口,触发并等待三次握手完成,成功后客户端套接字进入 ESTABLISHED 状态;
  2. 客户端套接字无需提前 bind(),内核会自动分配 IP 和临时端口;
  3. 阻塞模式下 connect() 等待三次握手完成,非阻塞模式需通过 select()/epoll() 监听连接状态;
  4. 核心交互逻辑:客户端 connect() 触发三次握手 → 服务器内核将连接放入 ACCEPT 队列 → 服务器 accept() 取出连接,双方开始通信。

理解 connect() 的关键是抓住"客户端主动发起三次握手"这个核心,它和服务器的 accept() 是一一对应的------connect() 是"拨打电话",accept() 是"接起电话"。

我的理解:调用connect方法(谁要连接?连接谁?连接服务器的长度?)

判断是否连接成功

执行向服务器写逻辑:创建字符转用于接受输入,使用getline函数接受来自标准输入的参数,房子line中。

调用write函数,写到客户端的文件描述符中,指定写的内容,写的大小。

回显逻辑:创建缓冲区,调用read函数(从哪读?读到哪?读多少?)

判断是否读取成功。将读取到的buffer中的内容输出到屏幕。最后关闭文件。

Q:有点迷的是为啥也从socket里面读?这里的socket不是客户端的套接字吗?

A:先回答问题:因为服务器写到了socket里面,socket是客户端的套接字。

其实整个代码的执行逻辑是:服务器先运行,创建了监听套接字,不断地监听,并且也已经调用了accept方法,服务器运行到这里是不断阻塞的,就等待客户端的连接。随后客户端建立连接(客户端已经创建好了套接字),然后服务器就接收到了客户端的连接,并且创建了一个套接字用来接受accept方法的返回值。这里创建的套接字是共给服务器使用的,随后调用服务(传参就是服务器的套接字和客户端的套接字)

**至此第一版的demo已经设计完毕。**第一版的缺陷是,只允许单进程访问服务器,这显然不符合我们的常理对么?所以第二版引入多进程。

demo1 v2

相较于第一版,第二版只需要改一下服务端的逻辑
server.hpp

cpp 复制代码
#pragma once
#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"

using namespace LogModule;
const static int defaultsockfd = -1;
const static int backlog = 8;
class TcpServer : public NoCopy
{
public:
    TcpServer(uint16_t port) : _port(port), _listensockfd(defaultsockfd), _isrunning(false)
    {
    }

    void Init()
    {
        // 回收子进程做法1. 
        // signal(SIGCHLD, SIG_IGN); // 父进程忽略子进程信号,让内核帮着回收子进程
        // 1. 创建套接字,socket类比open
        _listensockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket error";
            exit(SOCKET_ERR);
        }
        LOG(LogLevel ::INFO) << "socket success: " << _listensockfd;

        // 2. bind端口号
        InetAddr local(_port);
        int n = bind(_listensockfd, local.NetAddrPtr(), local.NetAddrLen());
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bing error";
            exit(BIND_ERR);
        }
        LOG(LogLevel ::INFO) << "bind success: " << _listensockfd;

        // 3. 设置socket状态
        n = listen(_listensockfd, backlog);
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "listen error";
            exit(LISTEN_ERR);
        }
        LOG(LogLevel ::INFO) << "listen success: " << _listensockfd;
    }
    
    void Service(int sockfd, InetAddr &peer)
    {
        char buffer[1024];
        while (true)
        {
            // 1. 先读取数据
            ssize_t n = read(sockfd, buffer, sizeof(buffer - 1));
            if (n > 0)
            {
                // 给buffer设置成C风格的字符串
                buffer[n] = 0;
                LOG(LogLevel::DEBUG) << peer.StringAddr() << "say# " << buffer;
                // 2. 写回数据
                std::string echo_string = buffer;
                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            else if (n == 0)
            {
                LOG(LogLevel::DEBUG) << peer.StringAddr() << "退出了...";
                close(sockfd);
                break;
            }
            else
            {
                LOG(LogLevel::DEBUG) << peer.StringAddr() << "异常...";
                close(sockfd);
                break;
            }
        }
    }

    void Run()
    {
        _isrunning = true;
        while (_isrunning)
        {
            // 获取链接 :其实内核已经建立连接了,获取链接得意义是让程序员知道服务器被谁链接,方便通信
            struct sockaddr_in peer;
            socklen_t len = sizeof(sockaddr_in);
            int sockfd = accept(_listensockfd, CONV(peer), &len);
            if (sockfd < 0)
            {
                LOG(LogLevel::WARNING) << "accept error";
                continue;
            }
            InetAddr addr(peer);
            LOG(LogLevel ::INFO) << "accept success, peer addr: " << addr.StringAddr();

            // Service(sockfd, addr);
            pid_t id = fork();
            if(id < 0)
            {
                LOG(LogLevel::FATAL) << "fork error";
                exit(FORK_ERR);
            }
            else if(id == 0) // 子进程
            {
                close(_listensockfd); // 关掉不想让子进程访问的套接字,关闭文件描述符
                // 回收子进程方法2:
                if(fork() > 0) // 子进程再次创建子进程,也就是让孙子进程去执行业务
                    exit(OK);

                Service(sockfd, addr); //孙子进程处理业务,孙子进程变成了孙子进程,被1号进程领养,被系统回收
                exit(OK);
            }
            else
            {
                close(sockfd); // 父进程管监听就行,不用管其他的套接字。
                pid_t rid = waitpid(id, nullptr, 0); // 回收子进程,采用方法2已经不会再阻塞,因为子进程瞬间退出了
                (void)rid;
            }
        }
        _isrunning = false;
    }

    ~TcpServer() {}

private:
    uint16_t _port;
    int _listensockfd;
    bool _isrunning;
};

解释

思想:在执行业务之前,创建子进程,让子进程去执行业务,父进程持续的监听客户端。如此就能实现多进程访问服务器。根本原因是:父子进程并发运行。

对于子进程来说,是不要和监听作用隔离的,也就是不想让子进程访问监听套接字,所以关闭对应的文件描述符。

然后去执行对应的业务。

这里的问题是父进程要等待子进程,我们知道,父进程等待子进程是阻塞等待,如果阻塞等待的话,就有变成了串行,还是解决不了多客户端访问的问题。

对于这个问题有两个解决办法:

  1. 忽略子进程的信号
  2. 创建孙子进程
    对于方法二,注释已经写的很清楚,再次不多赘述。

到此就完成了demo的第二版,多进程访问服务器。

下面第三版是多线程访问服务器。

demo1 v3

server.hpp

cpp 复制代码
#pragma once
#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"

using namespace LogModule;
const static int defaultsockfd = -1;
const static int backlog = 8;
class TcpServer : public NoCopy
{
public:
    TcpServer(uint16_t port) : _port(port), _listensockfd(defaultsockfd), _isrunning(false)
    {
    }

    void Init()
    {
        // 1. 创建套接字,socket类比open
        _listensockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket error";
            exit(SOCKET_ERR);
        }
        LOG(LogLevel ::INFO) << "socket success: " << _listensockfd;

        // 2. bind端口号
        InetAddr local(_port);
        int n = bind(_listensockfd, local.NetAddrPtr(), local.NetAddrLen());
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bing error";
            exit(BIND_ERR);
        }
        LOG(LogLevel ::INFO) << "bind success: " << _listensockfd;

        // 3. 设置socket状态
        n = listen(_listensockfd, backlog);
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "listen error";
            exit(LISTEN_ERR);
        }
        LOG(LogLevel ::INFO) << "listen success: " << _listensockfd;
    }

    class ThreadData
    {
    public:
        ThreadData(int fd, InetAddr &ar, TcpServer *s) : sockfd(fd), addr(ar), tsvr(s)
        {
        }
        int sockfd;
        InetAddr addr;
        TcpServer *tsvr;
    };

    void Service(int sockfd, InetAddr &peer)
    {
        char buffer[1024];
        while (true)
        {
            // 1. 先读取数据
            ssize_t n = read(sockfd, buffer, sizeof(buffer - 1));
            if (n > 0)
            {
                // 给buffer设置成C风格的字符串
                buffer[n] = 0;
                LOG(LogLevel::DEBUG) << peer.StringAddr() << "say# " << buffer;
                // 2. 写回数据
                std::string echo_string = buffer;
                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            else if (n == 0)
            {
                LOG(LogLevel::DEBUG) << peer.StringAddr() << "退出了...";
                close(sockfd);
                break;
            }
            else
            {
                LOG(LogLevel::DEBUG) << peer.StringAddr() << "异常...";
                close(sockfd);
                break;
            }
        }
    }

    static void *Routine(void *args)
    {
        pthread_detach(pthread_self());
        ThreadData *td = static_cast<ThreadData *>(args);
        td->tsvr->Service(td->sockfd, td->addr);
        delete td;
        return nullptr;
    }

    void Run()
    {
        _isrunning = true;
        while (_isrunning)
        {
            // 获取链接 :其实内核已经建立连接了,获取链接得意义是让程序员知道服务器被谁链接,方便通信
            struct sockaddr_in peer;
            socklen_t len = sizeof(sockaddr_in);
            int sockfd = accept(_listensockfd, CONV(peer), &len);
            if (sockfd < 0)
            {
                LOG(LogLevel::WARNING) << "accept error";
                continue;
            }
            InetAddr addr(peer);
            LOG(LogLevel ::INFO) << "accept success, peer addr: " << addr.StringAddr();

            ThreadData *td = new ThreadData(sockfd, addr, this);
            pthread_t tid;
            pthread_create(&tid, nullptr, Routine, td);
        }
        _isrunning = false;
    }

    ~TcpServer() {}

private:
    uint16_t _port;
    int _listensockfd;
    bool _isrunning;
};

demo2

该版本继续对上面升级,使用多线程执行具体的业务(远程执行命令行)。

点我查看源代码^ _ ^


相关推荐
i建模4 小时前
在 Rocky Linux 上安装轻量级的 XFCE 桌面
linux·运维·服务器
麻辣长颈鹿Sir4 小时前
TCP/IP四层架构通俗理解及功能介绍
网络协议·tcp/ip·tcp/ip协议四层架构·网络通信介绍
云边云科技_云网融合5 小时前
AIoT智能物联网平台:架构解析与边缘应用新图景
大数据·网络·人工智能·安全
若风的雨5 小时前
WC (Write-Combining) 内存类型优化原理
linux
YMWM_5 小时前
不同局域网下登录ubuntu主机
linux·运维·ubuntu
若风的雨5 小时前
NCCL 怎么解决rdma 网卡本地send的时候需要对端recv要准备好的问题,或者mpi 怎么解决的?
网络
zmjjdank1ng5 小时前
restart与reload的区别
linux·运维
哼?~5 小时前
进程替换与自主Shell
linux
浩浩测试一下5 小时前
DDOS 应急响应Linux防火墙 Iptable 使用方式方法
linux·网络·安全·web安全·网络安全·系统安全·ddos
2501_915918415 小时前
HTTPS 代理失效,启用双向认证(mTLS)的 iOS 应用网络怎么抓包调试
android·网络·ios·小程序·https·uni-app·iphone