编程语言
330
TCP是一个面向连接的,安全的,流式传输协议,这个协议是一个传输层协议。
- 面向连接:是一个双向连接,通过三次握手完成,断开连接需要通过四次挥手完成。
- 安全:tcp通信过程中,会对发送的每一数据包都会进行校验, 如果发现数据丢失, 会自动重传
- 流式传输:发送端和接收端处理数据的速度,数据的量都可以不一致
1. 服务器端通信流程
- 创建用于监听的套接字, 这个套接字是一个文件描述符
int lfd = socket();
- 将得到的监听的文件描述符和本地的IP 端口进行绑定
bind();
- 设置监听(成功之后开始监听, 监听的是客户端的连接)
listen();
- 等待并接受客户端的连接请求, 建立新的连接, 会得到一个新的文件描述符(通信的),没有新连接请求就阻塞
int cfd = accept();
- 通信,读写操作默认都是阻塞的
// 接收数据 read(); / recv(); // 发送数据 write(); / send();
- 断开连接, 关闭套接字
close();
在tcp的服务器端, 有两类文件描述符
- 监听的文件描述符
- 只需要有一个
- 不负责和客户端通信, 负责检测客户端的连接请求, 检测到之后调用accept就可以建立新的连接
- 通信的文件描述符
- 负责和建立连接的客户端通信
- 如果有N个客户端和服务器建立了新的连接,通信的文件描述符就有N个,每个客户端和服务器都对应一个通信的文件描述符
- 文件描述符对应的内存结构:
一个文件文件描述符对应两块内存, 一块内存是读缓冲区, 一块内存是写缓冲区
- 读数据:
通过文件描述符将内存中的数据读出, 这块内存称之为读缓冲区
- 写数据:
通过文件描述符将数据写入到某块内存中, 这块内存称之为写缓冲区
- 监听的文件描述符:
- 客户端的连接请求会发送到服务器端监听的文件描述符的读缓冲区中
- 读缓冲区中有数据, 说明有新的客户端连接
- 调用accept()函数, 这个函数会检测监听文件描述符的读缓冲区
- 检测不到数据, 该函数阻塞
- 如果检测到数据, 解除阻塞, 新的连接建立
- 通信的文件描述符:
- 客户端和服务器端都有通信的文件描述符
- 发送数据:调用函数 write() / send(),数据进入到内核中
- 数据并没有被发送出去, 而是将数据写入到了通信的文件描述符对应的写缓冲区中
- 内核检测到通信的文件描述符写缓冲区中有数据, 内核会将数据发送到网络中
- 接收数据: 调用的函数 read() / recv(), 从内核读数据
- 数据如何进入到内核我们不需要处理, 数据进入到通信的文件描述符的读缓冲区中
- 数据进入到内核, 必须使用通信的文件描述符, 将数据从读缓冲区中读出即可
基于tcp的服务器端通信代码:
// server.c #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> int main() { // 1. 创建监听的套接字 int lfd = socket(AF_INET, SOCK_STREAM, 0); if(lfd == -1) { perror("socket"); exit(0); } // 2. 将socket()返回值和本地的IP端口绑定到一起 struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(10000); // 大端端口 // INADDR_ANY代表本机的所有IP, 假设有三个网卡就有三个IP地址 // 这个宏可以代表任意一个IP地址 // 这个宏一般用于本地的绑定操作 addr.sin_addr.s_addr = INADDR_ANY; // 这个宏的值为0 == 0.0.0.0 // inet_pton(AF_INET, "192.168.237.131", &addr.sin_addr.s_addr); int ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr)); if(ret == -1) { perror("bind"); exit(0); } // 3. 设置监听 ret = listen(lfd, 128); if(ret == -1) { perror("listen"); exit(0); } // 4. 阻塞等待并接受客户端连接 struct sockaddr_in cliaddr; int clilen = sizeof(cliaddr); int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &clilen); if(cfd == -1) { perror("accept"); exit(0); } // 打印客户端的地址信息 char ip[24] = {0}; printf("客户端的IP地址: %s, 端口: %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ip, sizeof(ip)), ntohs(cliaddr.sin_port)); // 5. 和客户端通信 while(1) { // 接收数据 char buf[1024]; memset(buf, 0, sizeof(buf)); int len = read(cfd, buf, sizeof(buf)); if(len > 0) { printf("客户端say: %s\n", buf); write(cfd, buf, len); } else if(len == 0) { printf("客户端断开了连接...\n"); break; } else { perror("read"); break; } } close(cfd); close(lfd); return 0; }
2. 客户端的通信流程
在单线程的情况下客户端通信的文件描述符有一个, 没有监听的文件描述符
- 创建一个通信的套接字
int cfd = socket();
- 连接服务器, 需要知道服务器绑定的IP和端口
connect();
- 通信
// 接收数据 read(); / recv(); // 发送数据 write(); / send();
- 断开连接, 关闭文件描述符(套接字)
close();
基于tcp通信的客户端通信代码:
// client.c #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> int main() { // 1. 创建通信的套接字 int fd = socket(AF_INET, SOCK_STREAM, 0); if(fd == -1) { perror("socket"); exit(0); } // 2. 连接服务器 struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(10000); // 大端端口 inet_pton(AF_INET, "192.168.237.131", &addr.sin_addr.s_addr); int ret = connect(fd, (struct sockaddr*)&addr, sizeof(addr)); if(ret == -1) { perror("connect"); exit(0); } // 3. 和服务器端通信 int number = 0; while(1) { // 发送数据 char buf[1024]; sprintf(buf, "你好, 服务器...%d\n", number++); write(fd, buf, strlen(buf)+1); // 接收数据 memset(buf, 0, sizeof(buf)); int len = read(fd, buf, sizeof(buf)); if(len > 0) { printf("服务器say: %s\n", buf); } else if(len == 0) { printf("服务器断开了连接...\n"); break; } else { perror("read"); break; } sleep(1); // 每隔1s发送一条数据 } close(fd); return 0; }