Linux网络:socket编程TCP


前言

TCP 的通信过程就像两个人打电话:客户端先和服务端三次握手建立一条可靠的连接通道,之后数据会以字节流的形式在这条通道里双向传输,系统会负责把数据切片、编号、确认和重传,保证信息不丢失、不重复、按顺序送达,最后通过四次挥手优雅地断开连接。


连接流程大致如下图
在这里插入图片描述

一,服务器端流程

1-1 绑定协议

TCP通信也和UDP一样需要先创建套接字

int server_fd = socket(AF_INET, SOCK_STREAM, 0);

这里的SOCK_STREAM,说明套接字是用字节流的形式传递数据,也就是TCP通信


1-2 绑定IP和端口

sockaddr_in addr {};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY; // 本机任意 IP
addr.sin_port = htons(12345); // 端口 12345
if (bind(server_fd, (sockaddr * ) & addr, sizeof(addr)) < 0) {
    perror("bind");
    return 1;
}

addr.sin_family = AF_INET;的意思就是,设置协议为IPv4,其它的不过多讲解就是绑定端口和将sockaddr_in转化为sockaddr

addr.sin_addr.s_addr = INADDR_ANY表示本机的客户端的任意IP都可以连接服务端


1-3 监听客户端

listen(server_fd, 5);
std::cout << "服务端启动,等待客户端连接..." << std::endl;

server_fd是我们socket创建的套接字,用于服务端得知是哪一个套接字要监听,所以必须传入 server_fd。没有 server_fd,内核就不知道“我要监听哪条网络通道”,就没法接受连接

这里的这个参数5表明:连接队列的最大长度,内核会维护一个队列,存放已到达但还没被 accept() 处理的客户端连接,这里写 5 表示最多允许排队 5 个连接请求。超过的请求可能会被拒绝。


1-4 接收连接

sockaddr_in client_addr{};
socklen_t len = sizeof(client_addr);
int client_fd = accept(server_fd, (sockaddr*)&client_addr, &len);

我们需要接收客户端的连接,这时会用到 sockaddr_in 来存储客户端的地址信息。accept() 会返回一个新的套接字 client_fd。因此服务器中会有 两种套接字:

  • server_fd:用于监听端口和接受客户端的连接请求。它可以同时管理多个客户端连接,但 不能直接用于数据通信。

  • client_fd:由 accept() 返回,用于和 特定客户端 进行通信。每个客户端连接都会对应一个独立的 client_fd,可以通过 read()write() 发送或接收数据。

简而言之:server_fd 用于建立连接,client_fd 用于通信,server_fd 可以服务多个客户端,而每个 client_fd 只对应一个客户端。


1-5 收发数据

char buffer[1024];
int n = read(client_fd, buffer, sizeof(buffer) - 1);
if (n > 0) {
    buffer[n] = '\0';
    std::cout << "收到客户端消息: " << buffer << std::endl;
    std::string reply = "Hello from server!";
    write(client_fd, reply.c_str(), reply.size());
}

先是读取数据,我们先定义一个buffer,将对应客户端的套接字对应的文件信息读取到buffer中,再使用write将数据发送到client_fd对应的套接字当中,这里其实没什么特别的,就是收数据/发数据


1-6 关闭连接

close(client_fd);
close(server_fd);

虽然只有一行,但是OS这里会让客户端和服务端发生四次挥手:

  • FIN(发送方): 服务端调用 close(client_fd)内核向客户端发送 FIN 报文,表示“我已经没有数据要发了”。

  • ACK(接收方): 客户端收到 FIN 后,回复 ACK 报文,确认收到,客户端仍然可以发送剩余数据。

  • FIN(接收方): 客户端发送完数据后,也调用 close(),向服务端发送 FIN 报文,表示“我也发送完了”。

  • ACK(发送方): 服务端收到 FIN 后,发送 ACK 报文 确认。连接真正关闭。


1-7 服务端整体代码

int main() {
    // 1. 创建套接字
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
        perror("socket");
        return 1;
    }

    // 2. 绑定 IP 和端口
    sockaddr_in addr {};
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY; // 本机任意 IP
    addr.sin_port = htons(12345); // 端口 12345
    if (bind(server_fd, (sockaddr * ) & addr, sizeof(addr)) < 0) {
        perror("bind");
        return 1;
    }

    // 3. 监听
    listen(server_fd, 5);
    std::cout << "服务端启动,等待客户端连接..." << std::endl;

    // 4. 接受连接
    sockaddr_in client_addr {};
    socklen_t len = sizeof(client_addr);
    int client_fd = accept(server_fd, (sockaddr * ) & client_addr, & len);
    if (client_fd < 0) {
        perror("accept");
        return 1;
    }
    std::cout << "客户端已连接!" << std::endl;

    // 5. 收发数据
    char buffer[1024];
    int n = read(client_fd, buffer, sizeof(buffer) - 1);
    if (n > 0) {
        buffer[n] = '\0';
        std::cout << "收到客户端消息: " << buffer << std::endl;
        std::string reply = "Hello from server!";
        write(client_fd, reply.c_str(), reply.size());
    }

    // 6. 关闭连接
    close(client_fd);
    close(server_fd);
    return 0;
}

二,客户端流程

在客户端流程当中存在和服务端一样的流程,就是创建套接字,这里我们直接省略了。


2-1 指定地址和端口

sockaddr_in server_addr {};
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(12345);
inet_pton(AF_INET, "127.0.0.1", & server_addr.sin_addr); // 本地回环地址

这里我们创建的inet_pton的作用是把点分十进制的 IP 地址字符串转换成网络字节序的二进制形式

因为 socket 系统调用只能处理二进制的网络地址,而不能直接识别字符串形式的 IP,人类习惯用 "127.0.0.1" 这样的点分十进制 IP

内核底层在发送数据时,需要 32 位的二进制形式(网络字节序)来表示 IP 地址,inet_pton 就完成了 “人类可读 IP → 网络可用二进制 IP” 的转换,如果不做转换,connect()bind() 会因为地址无效而失败


2-2 连接服务器

我们通过connet来与服务端建立连接

if (connect(sock, (sockaddr * ) & server_addr, sizeof(server_addr)) < 0) {
    perror("connect");
    return 1;
}

这里没有什么很特别的,就是传递我们的地址端口接口体给服务端,然后服务端拿到结构体和客户端进行连接,并且客户端的sock套接字也和服务端的套接字对应的文件进行连接


2-3 发送消息

std::string msg = "Hello Server!";
write(sock, msg.c_str(), msg.size());
char buffer[1024];
int n = read(sock, buffer, sizeof(buffer) - 1);
if (n > 0) {
    buffer[n] = '\0';
    std::cout << "收到服务端回复: " << buffer << std::endl;
}

通过writeread发送和接收消息,也是很简单的代码


2-4 客户端整体代码

int main() {
    // 1. 创建套接字
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0) {
        perror("socket");
        return 1;
    }

    // 2. 指定服务端地址
    sockaddr_in server_addr {};
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(12345);
    inet_pton(AF_INET, "127.0.0.1", & server_addr.sin_addr); // 本地回环地址

    // 3. 连接服务器
    if (connect(sock, (sockaddr * ) & server_addr, sizeof(server_addr)) < 0) {
        perror("connect");
        return 1;
    }
    std::cout << "已连接服务器!" << std::endl;

    // 4. 发送消息
    std::string msg = "Hello Server!";
    write(sock, msg.c_str(), msg.size());

    // 5. 接收回复
    char buffer[1024];
    int n = read(sock, buffer, sizeof(buffer) - 1);
    if (n > 0) {
        buffer[n] = '\0';
        std::cout << "收到服务端回复: " << buffer << std::endl;
    }

    // 6. 关闭
    close(sock);
    return 0;
}

演示结果:
服务端

root@hcss-ecs-f59a:/gch/code/HaoHao/learn3/day6# ./server
服务端启动,等待客户端连接...
客户端已连接!
收到客户端消息: Hello Server!

客户端

root@hcss-ecs-f59a:/gch/code/HaoHao/learn3/day6# ./client
已连接服务器!
收到服务端回复: Hello from server!

本博客中只是做了一个简单的服务端和客户端的通信,在实际项目当中,这种通信代码还是前篇一律的,所以我就采用了分布式讲解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值