TCP和UDP的特点:
- 单播: 一对一通信,目标地址唯一。
- 多播: 一对多通信,目标地址是一个特定的组。
- 广播: 一对全部通信,目标地址是整个网络。
UDP不可靠的原因:
UDP(用户数据报协议)被认为是不可靠的主要是因为它缺乏一些保证数据可靠性的机制,这使得在传输过程中可能出现数据丢失、乱序或重复的情况。以下是导致UDP不可靠的一些主要原因:
1. 无连接性: UDP是一种无连接的协议,不需要在通信的两端建立和维护连接。这虽然提高了传输效率,但也意味着在传输过程中不进行状态检查和确认。
2. 不提供流控制: UDP不提供流控制机制,因此发送方会以最大速率发送数据,而不考虑接收方的处理能力。这可能导致数据包丢失或过载。
3. 不提供重传机制: 在UDP中,如果一个数据包在传输过程中丢失,协议本身不提供重传机制。这意味着丢失的数据包将不会被自动重新发送,而是由应用层来处理。
4. 不提供顺序控制: UDP也不保证数据包的传输顺序。数据包可能以不同的顺序到达接收方,这需要应用层来处理,特别是对于需要按顺序处理的数据。
5. 不提供拥塞控制: UDP不具备拥塞控制机制,因此在网络拥塞时可能导致数据包丢失。与TCP不同,UDP没有慢启动、拥塞避免等机制。
尽管UDP不可靠,但它的简单性和低开销使其在某些场景下非常有用,特别是对于实时性要求较高、对延迟敏感的应用,如音频和视频传输、在线游戏等。在这些情况下,轻量级的UDP可以提供更低的延迟,而可靠性方面则由应用层来处理。
TCP通信流程:
// TCP 通信的流程
// 服务器端 (被动接受连接的角色)
1. 创建一个用于监听的套接字
- 监听:监听有客户端的连接
- 套接字:这个套接字其实就是一个文件描述符
2. 将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息)
- 客户端连接服务器的时候使用的就是这个IP和端口
3. 设置监听,监听的fd开始工作
4. 阻塞等待,当有客户端发起连接,解除阻塞,接受客户端的连接,会得到一个和客户端通信的套接字
(fd)
5. 通信
- 接收数据
- 发送数据
6. 通信结束,断开连接
// 客户端
1. 创建一个用于通信的套接字(fd)
2. 连接服务器,需要指定连接的服务器的 IP 和 端口
3. 连接成功了,客户端可以直接和服务器通信
- 接收数据
- 发送数据
4. 通信结束,断开连接
套接字函数:
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h> // 包含了这个头文件,上面两个就可以省略
int socket(int domain, int type, int protocol);
- 功能:创建一个套接字
- 参数:
- domain: 协议族
AF_INET : ipv4
AF_INET6 : ipv6
AF_UNIX, AF_LOCAL : 本地套接字通信(进程间通信)
- type: 通信过程中使用的协议类型 // 流式协议和报式协议都不止一个协议,所以需要具体的协议
SOCK_STREAM : 流式协议
SOCK_DGRAM : 报式协议
- protocol : 具体的一个协议。一般写0
- SOCK_STREAM : 流式协议默认使用 TCP
- SOCK_DGRAM : 报式协议默认使用 UDP
- 返回值:
- 成功:返回文件描述符,操作的就是内核缓冲区。
- 失败:-1
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // socket命
名
- 功能:绑定,将fd 和本地的IP + 端口进行绑定
- 参数:
- sockfd : 通过socket函数得到的文件描述符
- addr : 需要绑定的socket地址,这个地址封装了ip和端口号的信息
- addrlen : 第二个参数结构体占的内存大小
- 返回值:
- 成功:0
- 失败:-1,并设置错误号
int listen(int sockfd, int backlog); // /proc/sys/net/core/somaxconn
- 功能:监听这个socket上的连接
- 参数:
- sockfd : 通过socket()函数得到的文件描述符
- backlog : 未连接的和已经连接的和的最大值, 5(不能超过机器规定好的最大值)
- 返回值:
- 成功:0
- 失败:-1,并设置错误号
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接
- 参数:
- sockfd : 用于监听的文件描述符
- addr : 传出参数,记录了连接成功后客户端的地址信息(ip,port)
- addrlen : 指定第二个参数的对应的内存大小
- 返回值:
- 成功 :用于通信的文件描述符(服务端套接字中的文件描述符只是用来监听的)
- -1 : 失败
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 功能: 客户端连接服务器
- 参数:
- sockfd : 用于通信的文件描述符
- addr : 客户端要连接的服务器的地址信息
- addrlen : 第二个参数的内存大小
- 返回值:成功 0, 失败 -1
ssize_t write(int fd, const void *buf, size_t count); // 写数据
ssize_t read(int fd, void *buf, size_t count); // 读数据
对两种协议的解释:
-
流式协议(Stream-oriented Protocol):
- 定义: 流式协议是一种将数据视为连续流(流式数据)的通信方式。数据被视为一串连续的字节,没有明确的分割点。
- 例子: TCP(传输控制协议)是流式协议的一个典型例子。在TCP通信中,数据被视为流动的字节序列,而不是独立的消息。
-
报式协议(Message-oriented Protocol):
- 定义: 报式协议是一种将数据划分为独立的消息或报文的通信方式。每个消息都是一个完整的数据单元,有明确的起始和结束。
- 例子: UDP(用户数据报协议)通常被认为是报式协议,因为它将数据划分为独立的数据包(报文),每个数据包都是一个完整的消息。
测试代码:
服务端:
// TCP 通信的服务器端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main() {
// 1.创建socket(用于监听的套接字)
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd == -1) {
perror("socket");
exit(-1);
}
// 2.绑定
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
// inet_pton(AF_INET, "172.22.2.64", saddr.sin_addr.s_addr);
saddr.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0,表示任意的地址
saddr.sin_port = htons(9999);
int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
if(ret == -1) {
perror("bind");
exit(-1);
}
// 3.监听
ret = listen(lfd, 8);
if(ret == -1) {
perror("listen");
exit(-1);
}
// 4.接收客户端连接
struct sockaddr_in clientaddr;
int len = sizeof(clientaddr);
int cfd = accept(lfd, (struct sockaddr *)&clientaddr, &len);
if(cfd == -1) {
perror("accept");
exit(-1);
}
// 输出客户端的信息
char clientIP[16];
inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, clientIP, sizeof(clientIP));
unsigned short clientPort = ntohs(clientaddr.sin_port);
printf("client ip is %s, port is %d\n", clientIP, clientPort);
// 5.通信
char recvBuf[1024] = {0};
while(1) {
// 获取客户端的数据
int num = read(cfd, recvBuf, sizeof(recvBuf));
if(num == -1) {
perror("read");
exit(-1);
} else if(num > 0) {
printf("recv client data : %s\n", recvBuf);
} else if(num == 0) {
// 表示客户端断开连接
printf("clinet closed...");
break;
}
char * data = "hello,i am server";
// 给客户端发送数据
write(cfd, data, strlen(data));
}
// 关闭文件描述符
close(cfd);
close(lfd);
return 0;
}
输出结果如下:
客户端的端口号通常是动态分配的,并且由操作系统自动选择一个可用的端口。因此,客户端自身的端口号不一定与服务端的端口号相同。 所以输出的不是9999端口。
客户端:
// TCP通信的客户端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main() {
// 1.创建套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1) {
perror("socket");
exit(-1);
}
// 2.连接服务器端
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
inet_pton(AF_INET, "172.22.2.64", &serveraddr.sin_addr.s_addr);
serveraddr.sin_port = htons(9999);
int ret = connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
if(ret == -1) {
perror("connect");
exit(-1);
}
// 3. 通信
char recvBuf[1024] = {0};
while(1) {
char * data = "hello,i am client";
// 给客户端发送数据
write(fd, data , strlen(data));
sleep(1);
int len = read(fd, recvBuf, sizeof(recvBuf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len > 0) {
printf("recv server data : %s\n", recvBuf);
} else if(len == 0) {
// 表示服务器端断开连接
printf("server closed...");
break;
}
}
// 关闭连接
close(fd);
return 0;
}
输出结果如下: