模型局限性:
1 不使用 非阻塞 同步异步 信号驱动 多路复用 select poll epoll 事件驱动 等技术
2 意在展示最原始的TCP模型
3 代码命名规整清晰,注释详尽,不官方,使用GPT4做了详细检查
实验流程:
服务器:socket->bind->listen->accept->recv->send->close 共7步
客户端:socket->connect->send->recv->close 共5步
预设共识:
只有一个服务器 可以有多个客户端 实际上后面的各种io模型变种都是基于此共识
实验描述及现象:
当客户端连接上服务器后,会输出已连接字样,并打印收到的数据
然后回给客户端一句话 为了模拟大数据处理 启用了5秒计时
客户端会收到这些数据并打印在自己的终端
这样就模拟了一次CS模型的TCP通信,可以反复运行客户端观察结果
观察阻塞现象:将客户端复制几份 多个控制台同时运行,可以观察到同时只有一个控制台在数54321
注意事项:
1 用于通信的ip地址和端口号要进行大小端转换
2 服务器 accept会返回新的socket用于读写,而客户端一直使用同一个socket
3 如果服务器和客户端都使用whlie(recv)的结构会造成类似死锁的现象,所以必须有一端缓冲区足够大
4 客户端进程的结束取决于服务器的处理速度,当服务器send全部数据后会close socket,客户端会收到0个字节后结束进程
5 服务器ip地址及端口号 已经预设到宏中 方便修改 必须同时修改CS两端
6 在更复杂的应用场景中,可能需要引入更高级的通信模型和技术,以提高效率和响应性。
#define _GNU_SOURCE
#include <stdio.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
// 服务器地址
#define SERVER_IP "192.168.142.132"
// 服务器端口
#define SERVER_PORT 55555
int main()
{// server_sockfd用于socket的返回值,client_sockfd用于accept的返回值int server_sockfd, client_sockfd;// 不用struct sockaddr的原因是不能直接设置ip和端口,而后都需要强转,属于历史遗留问题// 这个结构体主要存 ip地址 端口号 地址族struct sockaddr_in server_sockaddr, client_sockaddr;memset(&server_sockaddr, 0, sizeof(server_sockaddr));memset(&client_sockaddr, 0, sizeof(client_sockaddr));// accept要求填入lensocklen_t client_sockaddr_len = sizeof(client_sockaddr);// 表示发送接收的字节ssize_t send_bytes, recv_bytes;// 发送用bufchar send_buf[1024] = "server say : fine.";// 接受用bufchar recv_buf[1024] = {0};// 大小端转换用bufchar ipv4_addr_buf[64] = {0};// 新建ipv4 tcp类型socketserver_sockfd = socket(AF_INET, SOCK_STREAM, 0);if (server_sockfd == -1){perror("socket");exit(EXIT_FAILURE);}// 地址族server_sockaddr.sin_family = AF_INET;// ip地址大小端转换+存入结构体inet_pton(AF_INET, SERVER_IP, &server_sockaddr.sin_addr.s_addr);// 端口大小端转换+存入结构体server_sockaddr.sin_port = htons(SERVER_PORT);// 绑定到socketif (bind(server_sockfd, (struct sockaddr *)&server_sockaddr, sizeof(server_sockaddr)) == -1){perror("bind");exit(EXIT_FAILURE);}// 使用ss -tuln命令 可以看见此进程正在监听55555if (listen(server_sockfd, 16) == -1){perror("listen");exit(EXIT_FAILURE);}printf("server start...\n");while (1){// 有客户端connect,他就会accept// 函数返回后,代表建立了连接,此时client_sockaddr中会有内容// 函数的返回值是一个用于读写的新socketclient_sockfd = accept(server_sockfd, (struct sockaddr *)&client_sockaddr, &client_sockaddr_len);if (client_sockfd == -1){perror("accept");continue;}// 尝试打印出client_sockaddr中的内容,当然要进行字节序转换inet_ntop(AF_INET, &client_sockaddr.sin_addr, ipv4_addr_buf, sizeof(ipv4_addr_buf));printf("ipv4_addr:%s port:%d connected\n", ipv4_addr_buf, ntohs(client_sockaddr.sin_port));// 服务器收到的数据,假设缓冲区足够大能一次性读取recv_bytes = recv(client_sockfd, recv_buf, sizeof(recv_buf), 0);if (recv_bytes == -1){perror("recv");exit(EXIT_FAILURE);}else if (recv_bytes == 0){printf("closed by peer\n");continue;}// 打印出来看看printf("%s\n", recv_buf);// 假设有大型数据total字节,一次发不完ssize_t total = (ssize_t)strlen(send_buf);// 已发送数量ssize_t sent = 0;// 当已发送小于总数就继续发while (total > sent){// send_buf + sent 指针运算,发完的下次指针就往后挪// total - sent剩余数量send_bytes = send(client_sockfd, send_buf + sent, total - sent, 0);if (send_bytes == -1){perror("send");break;}// 计算已发送字节总数sent += send_bytes;}// 模拟大数据处理时间char a[5] = "54321";for (size_t i = 0; i < 5; i++){send(client_sockfd, &a[i], 1, 0);sleep(1);}// 这个客户端的请求就处理完了close(client_sockfd);}close(server_sockfd);return 0;
}
#define _GNU_SOURCE
#include <stdio.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>// 服务器地址
#define SERVER_IP "192.168.142.132"
// 服务器端口
#define SERVER_PORT 55555
int main()
{// 客户端用socketint client_sockfd;// 这个结构体主要存 ip地址 端口号 地址族struct sockaddr_in server_sockaddr, client_sockaddr;memset(&server_sockaddr, 0, sizeof(server_sockaddr));memset(&client_sockaddr, 0, sizeof(client_sockaddr));// getsockname要求填入lensocklen_t client_sockaddr_len = sizeof(client_sockaddr);// 表示发送接收的字节ssize_t send_bytes, recv_bytes;// 发送用bufchar send_buf[1024] = {0};// 接受用bufchar recv_buf[1024] = {0};// 地址转换用bufchar ipv4_addr_buf[64] = {0};// 新建ipv4 tcp类型socketclient_sockfd = socket(AF_INET, SOCK_STREAM, 0);if (client_sockfd == -1){perror("socket");exit(EXIT_FAILURE);}// 对server_sockaddr进行大小端转换及存入服务器地址信息inet_pton(AF_INET, SERVER_IP, &server_sockaddr.sin_addr.s_addr);server_sockaddr.sin_port = htons(SERVER_PORT);server_sockaddr.sin_family = AF_INET;// 连接服务器if ((connect(client_sockfd, (struct sockaddr *)&server_sockaddr, sizeof(server_sockaddr))) == -1){perror("connect");exit(EXIT_FAILURE);}// 获取本机地址及端口信息,端口是随机分配的getsockname(client_sockfd, (struct sockaddr *)&client_sockaddr, &client_sockaddr_len);inet_ntop(AF_INET, &client_sockaddr.sin_addr, ipv4_addr_buf, sizeof(ipv4_addr_buf));// 向服务器问好,并告诉是谁给他发的消息snprintf(send_buf, sizeof(send_buf), "ipv4_addr:%s port:%u say : how are you ?", ipv4_addr_buf, ntohs(client_sockaddr.sin_port));// 假设这句话是一个很大型的数据// 总字节ssize_t total = (ssize_t)strlen(send_buf);// 已发送字节ssize_t sent = 0;// 已发送小于总字节就继续发while (total > sent){// send_buf + sent 指针运算,发完的下次指针就往后挪// total - sent剩余数量send_bytes = send(client_sockfd, send_buf + sent, total - sent, 0);if (send_bytes == -1){perror("send");break;}// 计算已发送字节总数sent += send_bytes;}// 为什么客户端可以使用一个阻塞式的while(recv())?// 因为服务端发送完毕会close连接,客户端recv_bytes=0 跳出循环while ((recv_bytes = recv(client_sockfd, recv_buf, sizeof(recv_buf), 0)) > 0){printf("%s\n", recv_buf);memset(recv_buf, 0, sizeof(recv_buf));}if (recv_bytes == -1){perror("recv");}else if (recv_bytes == 0){printf("closed by peer\n");}// 全剧终close(client_sockfd);return 0;
}