文章目录
- 一、传输层协议TCP
- 二、通信流程
-
- [2.1 服务器端通信流程](#2.1 服务器端通信流程)
- [2.2 客户端的通信流程](#2.2 客户端的通信流程)
- 三、套接字代码
-
- [3.1 server.c](#3.1 server.c)
- [3.2 client.c](#3.2 client.c)
- [3.3 编译运行](#3.3 编译运行)
- [3.4 局限](#3.4 局限)
一、传输层协议TCP
特点: 面向连接的,安全的流式传输协议
- 面向连接
- 连接:三次握手,建立双向连接
- 断开:四次挥手,双向断开
- 安全的
- 在通信过程中会对数据包进行校验,判断对方有没有接收到发送的数据
- 如果数据没有被对方接收(数据丢失),就会对这个数据块进行重传
- 在通信过程中会对数据包进行校验,判断对方有没有接收到发送的数据
- 流式传输
- 接收和发送端处理的数据量可以是不对等的
- 举例:
- A 端每隔 5 s发送 4k 数据
- B 端每隔 1 s接收 100 字节
- 举例:
- 接收和发送端处理的数据量可以是不对等的
二、通信流程

2.1 服务器端通信流程
在服务器端有两类文件描述符:
- 监听的
- 监测有没有新的客户端连接
- 服务器端只需要一个就够了
- 通信的
- 负责和建立连接的客户端通信
- 和多少个客户端建立连接,通信的文件描述符就有多少个
套接字中的文件描述符
- 文件描述符对应内核中的两块内存
- 一个读缓冲区
- 一个写缓冲区
- 通信的文件描述符和监听的文件描述符对应的内核中的内存结构是一样的
- 监听的文件描述符
- 读缓冲区
- 客户端连接服务器,向服务器发送连接请求,这个请求数据进入到了服务器端的监听的文件描述符的读缓冲区
- 只要是这个缓冲区中有数据意味着有新的客户端连接
- 读缓冲区
- 通信的文件描述符
- 读缓冲区
- 保存的是对端发送过来的数据,通过调用读函数将数据从内核中读出来
ssize_t read(int fd, void *buf, size_t count);
- 保存的是对端发送过来的数据,通过调用读函数将数据从内核中读出来
- 写缓冲区
- 调用发送数据的函数,要发送的数据被写入到套接字对应的写缓冲区
ssize_t write(int fd, const void *buf, size_t count);
- 内核检测到写缓冲区中有数据,会将数据发送到网络的另一端,发送的数据会进到对端的通信文件描述符对应的读缓冲区
- 调用发送数据的函数,要发送的数据被写入到套接字对应的写缓冲区
- 读缓冲区
通信流程:
- 创建一个用于监听的套接字,这个套接字就是一个文件描述符
- 类似于管道中的文件描述符,对应是内核中的内存,通过文件描述符操作就可以读写内核中的内存数据
c
int lfd = socket();
- 让监听的文件描述符和本地的IP+端口进行绑定,为了让客户端找到服务器
- 绑定成功后,lfd 就可以监测到有没有客户端连接请求
c
bind();
- 给绑定成功的套接字设置监听
c
listen();
- 等待并接受客户端的连接,得到一个新的用于通信的文件描述符
c
int cfd = accept();
- 使用 accept 返回值对应的通信的文件描述符和客户端通信
- 接收数据
c
read();
recv();
- 发送数据
c
write();
send();
- 断开连接,关闭文件描述符
- 关闭通信的文件描述符:就不能通信
- 关闭监听的文件描述符:不饿能监测客户端连接
c
close();
2.2 客户端的通信流程
- 在 TCP 客户端文件描述符只有一种:通信的文件描述符
-
创建用于通信的套接字 == (文件描述符)
int fd = socket();
-
使用得到的通信的文件描述符连接服务器,通过服务器绑定的IP和端口进行连接
connect();
-
连接成功之后,通信
-
接收数据
read(); recv();
-
发送数据
write(); send();
-
断开和服务器的连接
close();
三、套接字代码
3.1 server.c
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. 绑定
struct sockaddr_in addr;
addr.sin_family = AF_INET; // IPv4
addr.sin_port = htons(8989); // 网络字节序
addr.sin_addr.s_addr = INADDR_ANY; // 0地址
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);
}
// 5. 通信
while (1) {
// 接收数据
char buf[1024];
memset(buf, 0, sizeof(buf));
int len = recv(cfd, buf, sizeof(buf), 0);
if (len == 0) {
printf("客户端已经断开连接...\n");
break;
} else if (len > 0) {
printf("recv buf: %s\n", buf);
// 回复数据
send(cfd, buf, strlen(buf)+1, 0); // +1 '\0'
} else {
perror("recv");
break;
}
}
// 6. 断开连接
close(cfd);
close(lfd);
return 0;
}
3.2 client.c
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main() {
// 1. 创建通信的套接字
int cfd = socket(AF_INET, SOCK_STREAM, 0);
if (cfd == -1) {
perror("socket");
exit(0);
}
// 2. 连接服务器
struct sockaddr_in addr;
addr.sin_family = AF_INET; // IPv4
addr.sin_port = htons(8989); // 网络字节序
// 将 192.168.69.141 转换为大端的整型数
inet_pton(AF_INET, "192.168.69.141", &addr.sin_addr.s_addr);
int ret = connect(cfd, (struct sockaddr*)&addr, sizeof(addr));
if (ret == -1) {
perror("connect");
exit(0);
}
// 3. 通信
int num = 0;
while (1) {
// 发送数据
char buf[1024];
sprintf(buf, "hello world, %d, ......", num++);
send(cfd, buf, strlen(buf) + 1, 0);
// 接收数据
memset(buf, 0, sizeof(buf));
int len = recv(cfd, buf, sizeof(buf), 0);
if (len == 0) {
printf("服务器已经断开连接...\n");
break;
} else if (len > 0) {
printf("recv buf: %s\n", buf);
} else {
perror("recv");
break;
}
sleep(1);
}
// 6. 断开连接
close(cfd);
return 0;
}
3.3 编译运行
- 编译命令
bash
gcc server.c -o s
gcc client.c -o c
- 运行
bash
./s
./c
3.4 局限
客户端并发只需要启动多个客户端即可,每启动一个客户端就得到一个进程,这每一个进程都会去连接服务器。
重点在服务器,服务器只能处理单个客户端的连接,如果要处理多个客户端的连接,那必须要有多线程或多进程。
阻塞函数accept
、recv
、send
这三类函数有任意一个阻塞了,程序就会阻塞,再有额外的客户端连接程序也处理不了。