对套接字的深入理解

为了方便理解,以下是一套完整的套接字编程

server.c

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <signal.h>

#define PORT 8888
#define BUFFER_SIZE 1024
#define BACKLOG 5      // 监听队列大小

// 忽略SIGPIPE信号,防止写已关闭的连接时进程终止
void handle_sigpipe() {
    struct sigaction sa;
    sa.sa_handler = SIG_IGN;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGPIPE, &sa, NULL);
}

int main() {
    int server_fd, client_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    char buffer[BUFFER_SIZE];
    ssize_t bytes_read;

    // 1. 忽略SIGPIPE
    handle_sigpipe();

    // 2. 创建TCP套接字
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == -1) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 3. 设置端口复用,避免"Address already in use"
    int opt = 1;
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) {
        perror("setsockopt failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 4. 绑定地址和端口
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;  // 监听所有网卡
    server_addr.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 5. 开始监听
    if (listen(server_fd, BACKLOG) == -1) {
        perror("listen failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    printf("Echo server listening on port %d...\n", PORT);

    // 6. 主循环:接受客户端连接
    while (1) {
        client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_addr_len);
        if (client_fd == -1) {
            perror("accept failed");
            continue;   // 继续等待下一个连接
        }

        // 打印客户端地址
        char client_ip[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN);
        printf("New connection from %s:%d\n", client_ip, ntohs(client_addr.sin_port));

        // 7. 处理当前客户端(循环接收并回显)
        while ((bytes_read = read(client_fd, buffer, sizeof(buffer) - 1)) > 0) {
            buffer[bytes_read] = '\0';   // 添加字符串结束符(可选,便于打印)
            printf("Received: %s", buffer);

            // 原样回送给客户端
            if (write(client_fd, buffer, bytes_read) == -1) {
                perror("write to client failed");
                break;
            }
        }

        if (bytes_read == -1) {
            perror("read error");
        } else if (bytes_read == 0) {
            printf("Client %s:%d closed connection\n", client_ip, ntohs(client_addr.sin_port));
        }

        close(client_fd);
    }

    // 理论上不会执行到这里,但为了完整性
    close(server_fd);
    return 0;
}

client.c

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 8888
#define BUFFER_SIZE 1024

int main(int argc, char *argv[]) {
    int sock_fd;
    struct sockaddr_in server_addr;
    char send_buf[BUFFER_SIZE];
    char recv_buf[BUFFER_SIZE];
    ssize_t bytes_sent, bytes_recv;

    // 检查命令行参数:需要提供服务器IP地址
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <server_ip>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    // 1. 创建TCP套接字
    sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (sock_fd == -1) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 2. 设置服务器地址结构
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    if (inet_pton(AF_INET, argv[1], &server_addr.sin_addr) <= 0) {
        perror("invalid address / address not supported");
        close(sock_fd);
        exit(EXIT_FAILURE);
    }

    // 3. 连接服务器
    if (connect(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("connect failed");
        close(sock_fd);
        exit(EXIT_FAILURE);
    }

    printf("Connected to server %s:%d\n", argv[1], PORT);
    printf("Enter messages (type 'quit' to exit):\n");

    // 4. 循环:从终端读取一行,发送给服务器,接收回显并打印
    while (1) {
        printf("> ");
        fflush(stdout);

        if (fgets(send_buf, sizeof(send_buf), stdin) == NULL) {
            break;  // EOF
        }

        // 去除末尾换行符(可选,但为了清楚显示)
        size_t len = strlen(send_buf);
        if (len > 0 && send_buf[len-1] == '\n') {
            send_buf[len-1] = '\0';
        }

        // 输入 "quit" 退出
        if (strcmp(send_buf, "quit") == 0) {
            break;
        }

        // 发送数据(注意:需重新加上换行符以便服务器输出美观,但也可以不加)
        // 这里简单发送原始字符串(不含换行),服务器会原样返回
        bytes_sent = write(sock_fd, send_buf, strlen(send_buf));
        if (bytes_sent == -1) {
            perror("write error");
            break;
        }

        // 接收回显
        bytes_recv = read(sock_fd, recv_buf, sizeof(recv_buf) - 1);
        if (bytes_recv == -1) {
            perror("read error");
            break;
        } else if (bytes_recv == 0) {
            printf("Server closed connection.\n");
            break;
        }

        recv_buf[bytes_recv] = '\0';
        printf("Echo: %s\n", recv_buf);
    }

    close(sock_fd);
    return 0;
}

在很多人看来,套接字是个很抽象的概念,大部分人只知道必须创建它才能进行通讯,但是套接字到底是什么,却不知道该怎么回答。如果你有以下疑问,我想我的思考会帮助到你。

1客户端服务器编程中客户端的套接字和服务器端的套接字有什么区别?

2如何去理解套接字

3客户端和服务器的套接字在通讯的时候都有什么变化,两个套接字是同一种吗?是同一个吗?

4监听套接字,连接套接字,已连接套接字有什么区别?

5在并发执行的情况下,套接字的行为是怎么样的?

以下是我的思考:

程序之间的连接需要一个媒介,这个媒介可以是文件描述符,管道,共享内存等,总之,它想连接就得有个媒介,那么程序在两个不同的主机上需要连接这个时候就可能需要借助网络,在通过网络进行连接的时候要满足网络模型,比如需要哪种连接是tcp还是udp,目的地在哪里?怎么知道数据该流向哪?这个时候就创造了套接字,套接字本身为觉得更像是个接口,用于网络中两主机之间程序的交互。在用socket函数创建套接字的时候,只需要给出协议类型和传输类型。这时的套接字就是包含了基本信息的普通的套接字。此时它只是一个本地的,未绑定的,未连接的端点,还不具备通信能力。

套接字本身都是socket函数创建出来的,所以一开始没有什么区别,只是不同的用法使得他们有了区别。一开始就是都由socket函数创建出来的普通套接字,服务器这边将套接字作为参数给listen后,才变成监听套接字,这个时候它已经绑定了端口和ip,就等待客户端的连接。这里根据行为不同有主动和被动的属性。socket() 创建的套接字是主动套接字,即它倾向于发起连接。服务器调用 listen() 后,主动套接字变为被动套接字,也就是监听套接字。客户端调用 connect() 后,主动套接字变成已连接套接字,直接用于 send/recv。

客户端通过 connect() 连接的是服务器端的 监听套接字(m_sockfd)。 一旦连接建立,服务器端会通过 accept() 生成一个 新的通信套接字(c),之后的收发都使用这个新套接字。

客户端自己的套接字在连接过程中不会改变 ;改变的是服务器端为每个新客户端生成一个独立的通信套接字。

客户端connect的时候,服务器这边在监听,将监听到的套接字放在半连接队列中,通过三次握手后将完成的套接字放在全连接队列中,accept负责从全连接队列中取一个已连接套接字,然后就建立了连接。connect函数在完成三次握手后执行完毕。双方可以开始交换数据。

客户端的已连接套接字和accept已经返回的已连接套接字本质上是同一个 TCP 连接的两个端点,是完全对等的套接字,但位于不同的主机上,并且生命周期略有差异。

也就是这两个套接字不是同一个套接字,只是用这两个已连接套接字进行通讯。这个时候的监听套接字就会继续接收别的客户端的连接然后继续三次握手。这个过程直到队列满后阻塞,如果只有一条执行路径还会阻塞在服务器端与客户端的数据交换上

这个时候为了达到高并发,就可以用某种方式,比如libevent库,将客户端与服务器的数据交换过程放在其他线程中,而主线程只负责监听来自客户端的连接。

res = listen(m_sockfd, m_lismax); 在这个函数里面的第二个参数就是全双工队列的最大连接数。

半连接队列不仅有大小,而且在高并发场景下,它的配置和管理,是需要严肃对待的关键环节,直接关系到服务器的稳定性和抗压能力。

以上就是我关于套接字的思考,感谢阅读

相关推荐
xyzzklk1 小时前
解决Salesforce无法向外发送邮件
android·java·开发语言·网络·crm·salesforce·客户关系管理
ZStack开发者社区2 小时前
VMware替代:从POC通过到生产可用,差距在哪里
服务器·云计算·gpu算力
hai3152475432 小时前
FlashAttention C语言(C++)实现(展示版)
c语言·开发语言·c++·人工智能·算法
AI创界者3 小时前
运维进阶:如何使用 Medusa 进行企业内部服务器密码合规性审计?
运维·服务器
wuminyu3 小时前
Java锁机制之Java对象重量级锁源码剖析
java·linux·c语言·jvm·c++
deadbird3 小时前
Xbox 无线适配器 Linux 设置指南
linux
珠***格4 小时前
实操落地|防逆流装置的安装规范、调试标准与故障处置
网络·数据库·人工智能·分布式·能源·边缘计算
郝学胜_神的一滴4 小时前
Qt 高级开发 026:QTabWidget御道,从筑基到化境
c++·qt
国科安芯4 小时前
国科安芯推出商业航天级抗辐照全双工 RS485/422 收发器 ASC491S2Y
网络·分布式·单片机·架构·安全性测试