计算机网络编程---系统调用到并发模型

目录

引言

一、Socket基础:地址族与类型

[1.1 Socket的本质](#1.1 Socket的本质)

[.2 地址族与协议族](#.2 地址族与协议族)

[1.3 套接字类型](#1.3 套接字类型)

二、TCP编程模型详解

[2.1 服务器端流程](#2.1 服务器端流程)

[2.2 客户端流程](#2.2 客户端流程)

[2.3 一个完整的TCP回显服务器](#2.3 一个完整的TCP回显服务器)

三、UDP编程模型

[3.1 服务器端](#3.1 服务器端)

[3.2 客户端](#3.2 客户端)

五、I/O多路复用:突破阻塞限制

[5.1 select](#5.1 select)

[5.2 poll](#5.2 poll)

[5.3 epoll(Linux特有)](#5.3 epoll(Linux特有))


引言

Socket(套接字)是Linux网络编程的基石,它提供了一种统一的接口,让应用程序能够通过网络与远端进程通信,也能在同一台主机内进行进程间通信。正如Linux的哲学"一切皆文件",Socket 本质上也是一种特殊的文件描述符。通过它,进程间可以像读写普通文件一样进行数据收发,操作系统则负责将数据封装为TCP段或UDP数据报,透明地穿越协议栈送达对端。

一、Socket基础:地址族与类型

1.1 Socket的本质

在Linux中,socket() 系统调用返回一个文件描述符,这个描述符关联着内核中的一套数据结构,包括发送缓冲区、接收缓冲区以及协议状态信息。应用层通过这个文件描述符进行操作,而内核负责处理传输层以下的复杂逻辑。

.2 地址族与协议族

创建Socket时需要指定地址族(Address Family),常见的包括:

  • AF_INET:IPv4网络协议,用于跨主机通信;

  • AF_INET6:IPv6网络协议,解决IPv4地址枯竭问题;

  • AF_UNIX:Unix域套接字,用于本机进程间通信,效率高于网络套接字。

1.3 套接字类型

Socket的类型决定了通信的语义:

  • SOCK_STREAM:流式套接字,基于TCP协议,提供可靠、有序、面向连接的双向字节流。适用于需要数据完整性的场景,如HTTP、FTP。

  • SOCK_DGRAM:数据报套接字,基于UDP协议,提供无连接、不可靠的消息传输。适用于视频流、DNS查询等对实时性要求高但能容忍部分丢包的场景。

  • SOCK_RAW:原始套接字,允许直接访问网络层及以下的数据包。常用于网络嗅探工具(如tcpdump)或实现自定义协议。

cpp 复制代码
#include <sys/socket.h>
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 创建一个IPv4的TCP套接字,第三个参数protocol通常为0,由系统自动选择

二、TCP编程模型详解

TCP是面向连接的协议,通信前必须建立连接。其编程模型类似于电话通信过程:安装电话机(socket)、分配号码(bind)、等待来电(listen)、接听电话(accept)。

2.1 服务器端流程

第一步:创建套接字

cpp 复制代码
int listenfd = socket(AF_INET, SOCK_STREAM, 0);

第二步:绑定地址

通过bind()将Socket与特定的IP地址和端口号关联。服务器通常需要显式绑定,以便客户端能够找到它

cpp 复制代码
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);  // 监听所有网络接口
servaddr.sin_port = htons(8080);                // 端口号,需转换为网络字节序

bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

第三步:监听连接

listen()将套接字设置为被动模式,准备接受连接请求。backlog参数指定了内核中已完成连接队列的最大长度

cpp 复制代码
listen(listenfd, 5);  // 允许最多5个连接排队等待

第四步:接受连接
accept()从已完成连接队列中取出一个连接。若队列为空,默认会阻塞。返回一个新的文件描述符connfd,用于与客户端通信;而原来的listenfd继续监听新连接。

cpp 复制代码
struct sockaddr_in cliaddr;
socklen_t clilen = sizeof(cliaddr);
int connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
// 此后用connfd进行读写,listenfd依然存在

第五步:数据交换

建立连接后,使用read()/write()send()/recv()进行数据传输。

cpp 复制代码
char buf[1024];
int n = read(connfd, buf, sizeof(buf));
write(connfd, buf, n);  // 简单的回显

第六步:关闭连接

cpp 复制代码
close(connfd);
close(listenfd);

2.2 客户端流程

客户端的步骤相对简单:

  1. socket()创建套接字;

  2. connect()向服务器发起连接请求,此调用会触发TCP三次握手;

  3. 连接成功后,通过send()/recv()write()/read()进行通信;

  4. close()关闭连接。

cpp 复制代码
int sockfd = socket(AF_INET, SOCK_STREAM, 0);

struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "192.168.1.100", &servaddr.sin_addr);
servaddr.sin_port = htons(8080);

connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

write(sockfd, "Hello", 5);
char buf[1024];
read(sockfd, buf, sizeof(buf));
close(sockfd);

2.3 一个完整的TCP回显服务器

下面给出一个可运行的TCP回显服务器示例,它不断接受客户端连接,将收到的消息原样返回:

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

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int listenfd, connfd;
    struct sockaddr_in servaddr, cliaddr;
    socklen_t clilen = sizeof(cliaddr);
    char buffer[BUFFER_SIZE];

    // 1. 创建socket
    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 2. 绑定地址
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = INADDR_ANY;
    servaddr.sin_port = htons(PORT);

    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("bind failed");
        close(listenfd);
        exit(EXIT_FAILURE);
    }

    // 3. 监听
    if (listen(listenfd, 5) < 0) {
        perror("listen failed");
        close(listenfd);
        exit(EXIT_FAILURE);
    }

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

    // 4. 接受连接并处理
    while (1) {
        connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
        if (connfd < 0) {
            perror("accept failed");
            continue;
        }
        printf("New client connected\n");

        // 5. 读取并回显
        ssize_t n = read(connfd, buffer, BUFFER_SIZE - 1);
        while (n > 0) {
            buffer[n] = '\0';
            printf("Received: %s\n", buffer);
            write(connfd, buffer, n);
            n = read(connfd, buffer, BUFFER_SIZE - 1);
        }

        if (n == 0) {
            printf("Client disconnected\n");
        } else if (n < 0) {
            perror("read error");
        }

        close(connfd);
    }

    close(listenfd);
    return 0;
}

三、UDP编程模型

UDP是无连接的,不需要三次握手。服务器和客户端之间更像"寄信"------知道地址即可发送。

3.1 服务器端

cpp 复制代码
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);

struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = INADDR_ANY;
servaddr.sin_port = htons(8080);
bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

struct sockaddr_in cliaddr;
char buf[1024];
socklen_t len = sizeof(cliaddr);

// 循环接收数据
int n = recvfrom(sockfd, buf, sizeof(buf), 0,
                 (struct sockaddr *)&cliaddr, &len);
// 可以向该客户端回复
sendto(sockfd, buf, n, 0, (struct sockaddr *)&cliaddr, len);

3.2 客户端

UDP客户端可以不调用bind(),操作系统会自动分配临时端口.

cpp 复制代码
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);

struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "192.168.1.100", &servaddr.sin_addr);
servaddr.sin_port = htons(8080);

char *msg = "Hello UDP";
sendto(sockfd, msg, strlen(msg), 0,
       (struct sockaddr *)&servaddr, sizeof(servaddr));

char buf[1024];
recvfrom(sockfd, buf, sizeof(buf), 0, NULL, NULL);

UDP的特点recvfrom()sendto()每次操作的是完整的数据报。若缓冲区小于数据报长度,多余数据会被截断丢弃。UDP不保证可靠性、顺序性,但在实时音视频、DNS等场景中凭借低延迟优势被广泛使用。

五、I/O多路复用:突破阻塞限制

上述示例中,一个进程同一时间只能处理一个客户端。要同时处理多个连接,需要I/O多路复用技术。

5.1 select

select()允许程序同时监控多个文件描述符的读、写或异常状态。它是POSIX标准的一部分,可移植性最好,但有以下限制:

  • 文件描述符数量受FD_SETSIZE(通常1024)限制;

  • 每次调用都需要将fd集合从用户态拷贝到内核态,效率随fd数量线性下降。

cpp 复制代码
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(listenfd, &readfds);
int maxfd = listenfd;

while (1) {
    fd_set tmpfds = readfds;
    select(maxfd + 1, &tmpfds, NULL, NULL, NULL);
    
    if (FD_ISSET(listenfd, &tmpfds)) {
        // 有新连接
        int connfd = accept(listenfd, ...);
        FD_SET(connfd, &readfds);
        if (connfd > maxfd) maxfd = connfd;
    }
    // 检查其他connfd是否有数据可读...
}

5.2 poll

poll()克服了select()的文件描述符数量限制,使用动态数组管理。但在大量文件描述符时,遍历整个数组仍存在O(n)的性能开销。

5.3 epoll(Linux特有)

epoll是Linux特有的I/O多路复用机制,专为高并发场景设计。它通过以下特性实现高效:

  • 事件驱动 :使用epoll_ctl()注册感兴趣的事件,内核通过回调机制在事件发生时将其加入就绪列表;

  • 零拷贝就绪列表epoll_wait()直接返回就绪的文件描述符,无需遍历全部;

  • 支持边缘触发(ET)和水平触发(LT) :ET模式只在状态变化时通知一次,效率更高;LT模式是默认模式,行为类似poll()

cpp 复制代码
int epfd = epoll_create1(0);

struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = listenfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);

struct epoll_event events[MAX_EVENTS];

while (1) {
    int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
    for (int i = 0; i < nfds; i++) {
        if (events[i].data.fd == listenfd) {
            int connfd = accept(listenfd, ...);
            ev.events = EPOLLIN | EPOLLET;  // 边缘触发模式
            ev.data.fd = connfd;
            epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
        } else {
            // 处理已连接套接字的数据
        }
    }
}
相关推荐
LinuxGeek10241 小时前
CVE-2026-31431 - Linux Copy-Fail 漏洞利用 (Rust版本)和检测方案
linux·运维·服务器
Season4501 小时前
C/C++的类型转换
c语言·开发语言·c++
Titan20241 小时前
C++特殊类设计
c++·学习
明日清晨1 小时前
有符号与无符号数转换
c++
learning-striving1 小时前
centos9安装docker测试成功教程
linux·运维·服务器·docker·容器
是wzoi的一名用户啊~1 小时前
Floyd 模版 弗洛伊德算法 模版
c++·算法·动态规划·图论·floyd
feng_you_ying_li1 小时前
linux之文件系统(3)
linux·运维·服务器
sbjdhjd1 小时前
Docker 网络工业级实战手册
linux·运维·经验分享·笔记·docker·云原生·云计算
gqk011 小时前
C++ / MFC / Qt / C# 核心知识点汇总笔记
c++·qt·mfc