上一节,我们学习了TCP协议的服务器-客户端的编程流程以及对中间的过程进行了详细的讨论,那么,这一节,我们对于TCP协议的特点进行进一步的分析,这也是面试的重点和难点。
目录
[一、TCP 协议特点](#一、TCP 协议特点)
[1.1 连接的建立与断开](#1.1 连接的建立与断开)
[1.1.1 面试题](#1.1.1 面试题)
[1.2 TCP 状态转移(面试题)](#1.2 TCP 状态转移(面试题))
[1.3 流式服务特点](#1.3 流式服务特点)
[1.4 应答确认与超时重传](#1.4 应答确认与超时重传)
[1.5 滑动窗口](#1.5 滑动窗口)
[3.1 UDP协议编程流程](#3.1 UDP协议编程流程)
[3.2 UDP 协议特点](#3.2 UDP 协议特点)
[3.3 应用场景](#3.3 应用场景)
[4.1 TCP和UDP的区别](#4.1 TCP和UDP的区别)
[4.2 同一个端口可不可以被一个 TCP 和一个 UDP 的应用程序同时使用?](#4.2 同一个端口可不可以被一个 TCP 和一个 UDP 的应用程序同时使用?)
[4.3 同一个应用程序可以创建多个套接字吗?](#4.3 同一个应用程序可以创建多个套接字吗?)
一、TCP 协议特点
通过前面的学习,我们知道:TCP 协议提供的是:面向连接、可靠的、字节流服务。
1.1 连接的建立与断开
使用 TCP 协议通信的双发必须先建立连接**(三次握手)** ,然后才能开始数据的读写。双方都必须为该连接分配必要的内核资源,以管理连接的状态和连接上数据的传输。TCP 连接是全双工的,双方的数据可以通过一个连接进行读写。完成数据交换之后,通信双方都必须断开连接以释放系统资源(四次挥手)。 使用tcpdump 抓包命令可以抓包观察 TCP 连接的建立与关闭。该命令需要管理员权限,格式如下(假设两个测试用的主机 IP 地址为 192.168.43.214 和 192.168.43.160 ) :

三次握手发生在客户端执行 connect()的时候,该方法返回成功,则说明三次握手已经建 立。三次握手示例图如下:
客户端执行connect()会给服务器发送一个tcp报文,此时SYN标志有效,还会发送一个序列号;服务器收到报文,会发送报文回复,此时SYN有效,发送一个序列号,还回复会一个确认号是客服端发送的序列号+1;客服端收到服务器的回复,也会再次回复服务器,此时会发送确认号是刚刚客户端发送的序列号+1 ;
四次挥手发生在客户端或服务端执行 close()关闭连接的时候,示例图如下:

当一端要进行close(),会给对方发送一个报文,此时FIN标志有效,还有一个序列号,然后对方收到报文,会回复对方已经收到了,发送一个确认号ACK,是刚刚发送的序列号+1;然后另一端也要close()关闭,也会给对方发送报文告诉对方字节要关闭了,FIN 序列号,对方收到报文了,会回复对方已经收到了,也发送一个确认号ACK,确认号是刚刚发送的序列号+1。
1.1.1 面试题
1、四次挥手的过程可以用三次完成吗?
可以,四次挥手可以演化成三次挥手 。
当一端close 发送报文过来,此时我也要close了,回复报文,和通知对方关闭的报文一起发送。
- 第一次挥手(FIN): 客户端发送一个FIN报文,表示它要关闭到服务器的数据传送。
- 第二次挥手(FIN): 服务器收到FIN后,直接发送一个FIN报文,表示它也要关闭到客户端的数据传送。
- 第三次挥手(ACK): 客户端收到FIN后,发送一个ACK报文,确认收到关闭请求,连接关闭。
2、挥手时,可能受到什么样的攻击?
FIN Flood 攻击:
- 原理:攻击者发送大量的FIN包到目标服务器。这些包会让服务器尝试关闭大量的连接,耗费资源处理这些无效的连接终止请求。
- 影响:服务器资源被耗尽,可能导致拒绝服务(DoS攻击)。
3、为什么是三次握手,可不可以是两次为什么?
握手只能是三次:例如客户端连接服务器后然后关闭了,服务器收到了并回复客户端,此时服务器就认为和客户端建立了链接,这个链接就一直保持着,但是客户端已经没了,所以还需要客户端第三次进行确认回复,来确保双方都保持链接。
4、三次握手时可能出现什么攻击?
SYN Flood 攻击:
- 原理:攻击者发送大量的SYN请求包到目标服务器,但不完成后续的握手步骤(即不发送ACK包)。目标服务器会为每个SYN请求分配资源并等待ACK回应,这样会导致服务器资源耗尽,无法处理合法用户的请求。(当有一个链接进来就会先放到未完成三次握手队列中,如果在短时间内有人连续发送链接就会把未完成三次握手队列塞满,使真正要进行链接的客户端连接不上。)
- 影响:造成服务器拒绝服务(DoS攻击)。
SYN ACK Flood 攻击:
- 原理:攻击者在没有发送初始SYN包的情况下,发送大量的SYN-ACK包到目标服务器。服务器会浪费资源去回复ACK,等待建立连接,导致资源耗尽。
- 影响:和SYN Flood类似,可能导致拒绝服务。
1.2 TCP 状态转移(面试题)
下图是 TCP 连接从建立到关闭整个过程中通信两端状态的变化。tcp状态的改变是在建立连接和断开连接的基础上的,其中 CLOSED 是假想的起始点,并不是一个实际的状态。这种状态变化就好比我们打电话通话处于不同的状态,但是只要双方拨通了电话,那么就一直是通话中。只有在拨打电话和挂断电话时,状态会发生变化。

上图中,TIME_WAIT 状态一般情况下是主动关闭的一端才会出现的状态。该状态出现后,会维持一段长为 2MSL(Maximum Segment Life)的时间,才能完全关闭。MSL 是 TCP 报文 段在网络中的最大生存时间,标准文档 RFC1122 的建议值是 2min。 在 Linux 系统上,一个 TCP 端口不能被同时打开多次(两次及以上)。当一个 TCP 连接 处于 TIME_WAIT 状态时,我们将无法立即使用该连接占用着的端口来建立一个新连接,必须要等待这两分钟过去,才能继续使用这个端口。
双方同时关闭:
双方都执行close(),都像对方发送FIN,双发都变成FIN_WAIT_1状态,等到双方都收到各自都收到对方发出的FIN,并发出ACK之后就会变成CLOSING状态,在等到双方都收到对方的ACK之后就会变成TIME_WAIT状态。
四次挥手演化成三次挥手:主动关闭端执行close(),发FIN,被动关闭端收到FIN,但此时被动关闭端也要关闭了,就把ACK和FIN一起发送给主动关闭端
connect()三次握手:客户端执行connect()后进行第一次进行握手发出SYN状态就变成了SYN_SENT状态,这个状态非常短暂,会观察不到,瞬间就没了。
服务器收到SYN后,又给客户端发出SYN,ACK后变成,SYN_RCVD状态。
服务器和客户端都完成三次握手状态就会变成,ESTABLISHED。
close()四次挥手:无论哪一方主动执行close()端,先发送FIN,然后主动关闭端就会变成FIN_WAIT_1状态,然后对方收到FIN,再发ACK就会变成CLOSE_WAIT状态,主动关闭端收到对方的回复,就变成了FIN_WAIT_2状态。此时两次挥手结束。被动关闭端执行close(),会给主动关闭端发送FIN,会变成LAST_ACK状态,主动关闭端收到FIN并发送ACK,主动关闭端状态就变成了TIME_WAIT,然后被动关闭段收到ACK,然后就消失了。
TIME_WAIT会持续大概两分钟的时间。


如上图所示:服务器会跟很多客户端有连接,每个连接都有自己的状态。每一个连接都会有自己的接收缓冲区和发送缓冲区。
使用命令:netstat -natp可以查看连接的状态


面试题:
为什么TIME_WAIT状态要持续一段时间?
1.可靠地终止TCP的连接。
2.保证让迟来的TCP报文段有足够的时间被识别并丢弃。
- 被动关闭端关闭发FIN,主动关闭端收到FIN,发ACK,变成TIME_WAIT,有可能被动关闭端没收到这个ACK,这个ACK在路上丢失了,过一会被动关闭端没收到主动关闭端的ACK就会再次发FIN,如果TIME_WAIT状态不持续直接关闭,那最后假如ACK丢失,被动关闭端在发送FIN就没人管它了。
- 在通讯的过程中,有一些数据正在发送,但还没发送到,数据正在从A端到B端但还没到,此时断开接收端和发送端的连接,之后这个延迟的数据包到达了,但此时连接已经断开了,就会出现一些问题尤其是服务器。如果没有TIME_WAIT状态,我们就可以立刻重新启动服务端,这样延迟的数据包就会陆陆续续发到我们这个新启动的服务器里,虽然我们新启动的服务器用的是这个ip这个端口,延迟的数据包用的也是这个ip和端口,但是这些数据包是发给上个已经结束的进程的,不是发给我们这个新进程的。因此就会让TIME_WAIT状态等待大概2分钟,这俩分钟是一个报文生存期最长时间的俩倍,这样就会把我们网络中延迟的数据包耗死,我们把这些延迟的数据一收延后丢掉,俩分钟后网络中就干净了。
题目:
一个局域网内,有一个客户端一个服务器,他们都已完成三次握手状态,没有发送数据,此时拔掉网线,服务器再close(),重新运行服务器,运行之后在插上网线,问此时客户端跟服务器的状态。
网线拔掉之后,不进行收发送数据,双方是不知道的,由于拔掉网线,关闭服务器,服务器会发送FIN,但是客户端收不到,也不会回复,服务器就等了俩分钟后就关闭了,再重新启动服务器,此时服务器就是LISTEN状态等待连接,客户端还是完成三次握手状态。
1.3 流式服务特点
TCP 字节流的特点,发送端执行的写操作次数和接收端执行的读操作次数之间没有任何数量关系,应用程序对数据的发送和接收是没有边界限制的。多次发送的数据会被对方一次接受,或者一次发送的数据被对方,分多次接受。
netstat -natp命令 可查看端口是否被占用 也能查看接收缓冲区和发送缓冲区有多少数据
- bind()会失败的原因 :端口被占用或者把这个程序运行了,又运行了一个,端口已经分给第一个运行的程序。
- recv()返回值为0是唯一判断对方客户端关闭链接的条件。
- connect()链接失败原因:没有运行服务器,客户端连接就会失败。网断了也链接不上。

cpp
修改循环收发的服务器端的代码如下:
char buff[128]={0};
recv(sockfd,buff,1,0);
/*一次只接收一个字符*/
客户端发个hello,服务器将接收字符个数改成1,出现的结果是:循环5次把hello打印完,直到把buff里的数据打印完。客服端那里会一次收到5个ok。

这是因为服务器和客户端都有一个接收缓冲区和发送缓冲区,一端send()发送数据,先把数据写到发送缓冲区里,再通过底层协议把发送缓冲区的数据挪到对方的接受缓冲区中,然后对方再通过recv()把接收缓冲区中的数据读出来。recv()发送成功只能说明成功将数据发达发送缓冲区,对方并没有收到。有可能会多次从接收缓冲区一次读取,也有可能分多次读取,就像我们购物从菜鸟驿站取快递,我们取出的快递件也可能一次取完,也有可能还没到菜鸟驿站,我们就需要分多次取。这就是****TCP 粘包: 连续多次send()发送的数据,被对方recv()一次性收到。发送数据的次数跟接收数据的次数是不对应的。所以会出现粘包。如何解决呢?(面试题)

解决粘包问题的常见方法有以下几种:(面试题)
1. 使用定长消息
通过规定每条消息的长度,接收方可以按照固定长度读取数据。例如,如果消息长度固定为100字节,接收方每次读取100字节的数据,就可以避免粘包问题。
2. 使用特殊分隔符
在每条消息的末尾添加特定的分隔符(如换行符、特殊字符等),接收方可以通过检测分隔符来区分消息边界。
3. 使用消息头(长度前缀)
在每条消息前添加一个消息头,用于存储消息的长度,接收方先读取消息头中的长度信息,再根据长度读取具体的消息内容。
1.4 应答确认与超时重传
TCP 发送的报文段是交给 IP 层传送的。但 IP 层只能提供尽最大努力的服务,也就是说,TCP 下面的网络所提供的是不可靠的传输。因此,TCP 必须采用适当的措施才能使两个运输层之间的通信变得可靠。 **TCP 的可靠传输是通过使用应答确认和超时重传来完成。**下图是通过 netstat 命令抓包看到的信息:

面试题:
tcp的可靠性体现在:应答确认、超时重传、去重、乱序重排、进行流量控制滑动窗口
- **应答确认:**给对方send()发送一个数据,对方收到了,在底层会回复发送方表明收到数据了,A端给B端发送数据,表面只能看到俩次交互,实际有四次,另外两次我们看不到,但可以用tcpdump抓包命令看到。
- **超时重传:**给对方发送数据收,等了一段事件后没有收到对方的回复,就认为这个数据丢失了,就会再重新发送一份数据给对方。
- **去重:**给对方发送数据,对方收到了,回复确认收到信息,但回复这个信息丢失了,发送段没收到,就会认为发送的数据在路上丢失了,就会重新发,然后接收端就会有俩个一样的数据,重复了就会去重。
- **乱序重排:**后发送的数据比先发送的数据先到达,这样顺序就会乱,但在接收到数据后,会对数据的顺序进行检查。
- **滑动窗口:**给对方发送数据,一个字节一个字节发效率不高,就会有一个窗口,窗口左边是已发送对方回复确认的数据,窗口内是有已发送未收到确认的和未发送的数据,窗口右边是超过窗口范围内外就不能发送的,窗口内比如能够发送100字节,我们20字节一个包,这样发送,发送20字节,没收到对方回复,我们还能继续发送,直到把这滑动窗口内的100字节数据全部发送完了,还没收到对方回复收到的信号,就不能再发送了,如果前面20字节,对方回复收到了,这个窗口就向后移动,确保窗口内数据有100个字节,然后新到窗口内的数据就能发送了。因为如果你光发送数据,也不知道对方收没收就到一直发;或者就是对方一次性只能接受多少数据,发太多也没用。
下图是无差错时,数据交互的流程:发送端发送数据 m1 给接收端,接收端收到数据后会给发送端一个确认信息,以表明数据已经被成功收到。在发送方未收到确认信息前,M1 应继续被保留,直到确认信息到达才能丢弃。

下图是出现差错时,数据交互的流程:

1.5 滑动窗口
TCP 协议是利用滑动窗口实现流量控制的。一般来说,我们总是希望数据传输得更快一些,不会一次只发一个字节。但是如果发送方把数据发得过快,接受方就可能来不及接收, 这就会造成数据的丢失。所谓流量控制就是让发送方的发送速率不要太快,要让接收方来的及接收。在 TCP 的报头中有一个字段叫做接收通告窗口,这个字段由接收端填充,是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。**所以发送端就会有一个发送窗口,这个发送窗口的大小是由接收端填充的接收通告窗口的大小决定的,并且窗口的位置会随着发送端数据的发送和接收到接收端对数据的确认而不断的向右滑动,将之称为滑动窗口。**发送方的滑动窗口示意图如下:

当收到 36 的 ack,并发出 46-51 的字节后,窗口滑动的示意图如下:

二、多进程、多线程处理并发
如下图所示, 当一个客户端与服务器建立连接以后,服务器端 accept()返回,进而准备循环接收客户端发过来的数据。如果客户端暂时没发数据,服务端会在第 40 行的 recv()阻 塞。此时,其他客户端向服务器发起连接后,由于服务器阻塞了,无法执行 accept()接受连 接,也就是其他客户端发送的数据,服务器无法读取。服务器也就无法并发同时处理多个客户端。


这个问题可以通过引入多线程和多进程来解决。**服务端接受一个客户端的连接后,创建 一个线程或者进程,然后在新创建的线程或进程中循环处理数据。主线程(父进程)只负责监听客户端的连接,并使用 accept()接受连接,不进行数据的处理。**如下图所示:

多线程处理并发的服务器端示例代码 MultiThread.c 如下:主线程负责监听端口和接受客户端连接,每接受到一个客户端连接后,就创建一个新线程来处理该客户端的通信。每个子线程会循环接收客户端发送的数据,并回复一个确认消息"ok"。当客户端断开连接时,子线程会关闭相应的客户端套接字并退出。
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
// 线程函数,用来处理单个客户端的收发数据
void* fun(void * arg)
{
int c = (int)arg; // 将传入的参数转换为整数类型的客户端套接字描述符
while( 1 )
{
char buff[128] = {0}; // 用于接收数据的缓冲区
// 接收客户端发送的数据,如果接收失败或连接关闭,则退出循环
if ( recv(c, buff, 127, 0) <= 0 )
{
break;
}
printf("recv(%d)=%s\n", c, buff); // 打印接收到的数据
send(c, "ok", 2, 0); // 发送确认消息给客户端
}
printf("one client over(%d)\n", c); // 打印客户端连接结束的消息
close(c); // 关闭客户端连接
}
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建套接字
assert(sockfd != -1); // 确认套接字创建成功
struct sockaddr_in saddr, caddr; // 定义服务器和客户端的地址结构
memset(&saddr, 0, sizeof(saddr)); // 将服务器地址结构清零
saddr.sin_family = AF_INET; // 设置地址族为AF_INET
saddr.sin_port = htons(6000); // 设置端口号为6000,并转换为网络字节序
saddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 设置IP地址为127.0.0.1
int res = bind(sockfd, (struct sockaddr*)&saddr, sizeof(saddr)); // 绑定套接字到指定的IP地址和端口
assert(res != -1); // 确认绑定成功
listen(sockfd, 5); // 开始监听,最大连接数为5
while( 1 )
{
int len = sizeof(caddr); // 客户端地址结构长度
// 接受客户端连接请求,返回客户端套接字描述符
int c = accept(sockfd, (struct sockaddr*)&caddr, &len);
if ( c < 0 )
{
continue; // 如果接受失败,继续等待下一个连接
}
printf("accept c = %d\n", c); // 打印接受到的客户端套接字描述符
pthread_t id; // 定义线程id
// 创建子线程处理客户端连接,传入客户端套接字描述符作为参数
pthread_create(&id, NULL, fun, (void*)c);
}
close(sockfd); // 关闭服务器套接字
exit(0); // 退出程序
}
多进程处理并发的服务器端示例代码 MultiProcess.c 如下:主进程负责监听端口和接受客户端连接,每接受到一个客户端连接后,创建一个子进程来处理该客户端的通信。子进程会循环接收客户端发送的数据,并回复一个确认消息"OK"。当客户端断开连接时,子进程会关闭相应的客户端套接字并退出。主进程通过捕捉SIGCHLD信号来处理子进程退出,防止产生僵尸进程。
cpp
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <signal.h>
// 处理客户端连接的函数
void DealClientLink(int c, struct sockaddr_in caddr)
{
while (1)
{
char buff[128] = {0}; // 用于接收数据的缓冲区
int n = recv(c, buff, 127, 0); // 接收客户端发送的数据
if (n <= 0) // 如果接收失败或客户端关闭连接,则退出循环
{
break;
}
// 打印客户端发送的数据,包括客户端的IP地址和端口号
printf("%s:%d %s\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port), buff);
send(c, "OK", 2, 0); // 发送确认消息给客户端
}
printf("one client unlink\n"); // 打印客户端断开连接的消息
close(c); // 关闭客户端连接
}
// 信号处理函数,用于处理子进程退出时的SIGCHLD信号
void sigfun(int sign)
{
wait(NULL); // 等待子进程结束,防止僵尸进程
}
int main()
{
signal(SIGCHLD, sigfun); // 注册SIGCHLD信号处理函数,处理僵尸进程
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建套接字
assert(-1 != sockfd); // 确认套接字创建成功
struct sockaddr_in saddr; // 定义服务器的地址结构
memset(&saddr, 0, sizeof(saddr)); // 将服务器地址结构清零
saddr.sin_family = AF_INET; // 设置地址族为AF_INET
saddr.sin_port = htons(6000); // 设置端口号为6000,并转换为网络字节序
saddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 设置IP地址为127.0.0.1
int res = bind(sockfd, (struct sockaddr*)&saddr, sizeof(saddr)); // 绑定套接字到指定的IP地址和端口
assert(-1 != res); // 确认绑定成功
listen(sockfd, 5); // 开始监听,最大连接数为5
while (1)
{
struct sockaddr_in caddr; // 定义客户端的地址结构
int len = sizeof(caddr); // 客户端地址结构长度
int c = accept(sockfd, (struct sockaddr*)&caddr, &len); // 接受客户端连接请求,返回客户端套接字描述符
assert(-1 != c); // 确认接受成功
// 打印接受到的客户端连接成功的消息,包括客户端的IP地址和端口号
printf("%s:%d Link Success\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
pid_t pid = fork(); // 创建子进程
assert(-1 != pid); // 确认子进程创建成功
if (0 == pid)
{
DealClientLink(c, caddr); // 子进程处理客户端连接
exit(0); // 必须结束子进程,否则会有多个进程调用accept
}
else
{
close(c); // 父进程关闭客户端连接描述符
}
}
close(sockfd); // 关闭服务器套接字
exit(0); // 退出程序
}
客户端代码 TcpClient.c 如下:客户端首先创建一个套接字,然后连接到指定IP地址和端口号的服务器。连接成功后,客户端进入一个循环,从标准输入获取用户输入的数据,并将其发送到服务器。随后,客户端接收服务器的响应并打印出来。如果用户输入"end",客户端会退出循环,关闭套接字并结束程序。
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建套接字
assert(sockfd != -1); // 确认套接字创建成功
struct sockaddr_in saddr; // 定义服务器的地址结构
memset(&saddr, 0, sizeof(saddr)); // 将服务器地址结构清零
saddr.sin_family = AF_INET; // 设置地址族为AF_INET
saddr.sin_port = htons(6000); // 设置端口号为6000,并转换为网络字节序
saddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 设置IP地址为127.0.0.1
int res = connect(sockfd, (struct sockaddr*)&saddr, sizeof(saddr)); // 连接到服务器
assert(res != -1); // 确认连接成功
while (1)
{
char buff[128] = {0}; // 用于存储用户输入的缓冲区
printf("input:\n"); // 提示用户输入
fgets(buff, 128, stdin); // 从标准输入获取用户输入
if (strncmp(buff, "end", 3) == 0) // 如果用户输入"end",则退出循环
{
break;
}
send(sockfd, buff, strlen(buff), 0); // 发送用户输入的数据到服务器
memset(buff, 0, 128); // 清空缓冲区
recv(sockfd, buff, 127, 0); // 接收服务器的响应
printf("buff=%s\n", buff); // 打印服务器的响应
}
close(sockfd); // 关闭套接字
exit(0); // 退出程序
}

三、UDP协议
3.1 UDP协议编程流程
**UDP 提供的是无连接、不可靠的、数据报服务。**可以通俗的将TCP理解成打电话,UDP理解成发短信。

socket()用来创建套接字,使用 udp 协议时,选择数据报服务 SOCK_DGRAM。sendto() 用来发送数据,由于 UDP 是无连接的,每次发送数据都需要指定对端的地址(IP 和端 口)。recvfrom()接收数据,每次都需要传给该方法一个地址结构来存放发送端的地址。 recvfrom()可以接收所有客户端发送给当前应用程序的数据,并不是只能接收某一个客户端的数据。
UDP 服务端编程示例代码:
cpp
1. #include <stdio.h>
2. #include <stdlib.h>
3. #include <unistd.h>
4. #include <string.h>
5. #include <assert.h>
6. #include <sys/socket.h>
7. #include <netinet/in.h>
8. #include <arpa/inet.h>
9.
10. int main()
11. {
12. int sockfd = socket(AF_INET,SOCK_DGRAM,0);
13. assert( sockfd != -1 );
14.
15. struct sockaddr_in saddr,caddr;
16. memset(&saddr,0,sizeof(saddr));
17. saddr.sin_family = AF_INET;
18. saddr.sin_port = htons(6000);
19. saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
20.
21. int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
22. assert( res != -1 );
23.
24. while( 1 )
25. {
26. int len = sizeof(caddr);
27. char buff[128] = {0};
28. recvfrom(sockfd,buff,127,0,(struct sockaddr*)&caddr,&len);
29. printf("ip:%s,port:%d,buff=%s\n",inet_ntoa(caddr.sin_addr), ntohs(caddr.si
n_port),buff );
30.
31. sendto(sockfd,"ok",2,0,(struct sockaddr*)&caddr,sizeof(caddr));
32. }
33.
34. close(sockfd);
35. }
UDP 客户端编程示例代码:
cpp
1. #include <stdio.h>
2. #include <stdlib.h>
3. #include <unistd.h>
4. #include <string.h>
5. #include <assert.h>
6. #include <sys/socket.h>
7. #include <netinet/in.h>
8. #include <arpa/inet.h>
9.
10. int main()
11. {
12. int sockfd = socket(AF_INET,SOCK_DGRAM,0);
13. assert( sockfd != -1 );
14.
15. struct sockaddr_in saddr;
16. memset(&saddr,0,sizeof(saddr));
17. saddr.sin_family = AF_INET;
18. saddr.sin_port = htons(6000);
19. saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
20.
21. while( 1 )
22. {
23. char buff[128] = {0};
24. printf("input:\n");
25.
26. fgets(buff,128,stdin);
27.
28. if ( strncmp(buff,"end",3) == 0 )
29. {
30. break;
31. }
32.
33. sendto(sockfd,buff,strlen(buff),0,(struct sockaddr*)&saddr,sizeof(saddr));
34. memset(buff,0,128);
35.
36. int len = sizeof(saddr);
37. recvfrom(sockfd,buff,127,0,(struct sockaddr*)&saddr,&len);
38.
39. printf("buff=%s\n",buff);
40. }
41.
42. close(sockfd);
43. }

启动服务端和客户端,再关掉服务端,还能再发送数据嘛?可以 因为udp是无连接的,只要服务端启动,有人发数据就接受。关掉服务端对客户端来说,丝毫没有影响
3.2 UDP 协议特点
**UDP 数据报服务特点:发送端应用程序每执行一次写操作,UDP 模块就将其封装成一 个 UDP 数据报发送。接收端必须及时针对每一个 UDP 数据报执行读操作,否则就会丢包,因此它不会出现粘包现象。**并且,如果用户没有指定足够的应用程序缓冲区来读取 UDP 数据,则 UDP 数据将被截断。

3.3 应用场景
tcp和udp应用分场景,例如下载一个文件,肯定是要完整的下载下来,数据不能丢失。如果实时通话视频时,那就用udp,因为只是要看当下的你,如果视频过程中网不好,数据没发出去,再重新发,这样慢慢的就会变成录屏,因为tcp有接收缓冲区,重新发的数据都会被对方,接收到接受缓冲区,对方要全部读完,所以这一帧数据没发送成功就不要了。
四、面试题
4.1 TCP和UDP的区别
tcp是面向连接的可靠的流式服务,udp是无连接不可靠的数据报服务 。
- tcp建立连接要进行三次握手,而udp不需要建立连接直接指定地址发数据就行
- tcp在发送数据时有应答确认,超时重传机制,而udp发送数据成功就成功,失败了也不会重发。
- tcp会出现粘包,udp不会出现粘包。
4.2 同一个端口可不可以被一个 TCP 和一个 UDP 的应用程序同时使用?
是的,可以,同一个端口可以同时被一个 TCP 应用程序和一个 UDP 应用程序使用。**TCP 和 UDP 是两个不同的传输层协议,它们的连接和数据传输方式不同,因此它们可以在相同的端口号上共存。操作系统和网络栈通过区分传输层协议(TCP 或 UDP)来将数据包正确地交付给对应的应用程序。**例如,假设你有一个 TCP 服务在端口 8080 上运行,同时你也可以在相同的端口 8080 上运行一个 UDP 服务。这两个服务不会互相干扰,因为操作系统能够根据协议类型将到达端口 8080 的 TCP 数据包和 UDP 数据包区分开来并分别处理。
4.3 同一个应用程序可以创建多个套接字吗?
同一个应用程序可以创建多个套接字。套接字是网络通信的基础,它允许程序发送和接收数据。应用程序创建多个套接字的原因有很多,包括但不限于以下几个方面:
多协议支持:一个应用程序可能需要同时支持多种协议,例如同时使用 TCP 和 UDP,这时它需要分别为 TCP 和 UDP 创建不同的套接字。
多端口监听:一个服务器应用程序可能需要监听多个端口,以便提供不同的服务或支持不同的协议版本。例如,一个应用程序可以同时监听 80 端口(HTTP)和 443 端口(HTTPS)。
客户端连接管理:对于一个 TCP 服务器,每当一个客户端连接到服务器时,服务器通常会为每个客户端连接创建一个新的套接字。这允许服务器同时处理多个客户端连接。
多线程或多进程通信:应用程序可能使用多个套接字来实现多线程或多进程间的通信。例如,一个线程或进程负责监听网络连接,另一个线程或进程负责处理数据。
至此,已经讲解完毕!篇幅较长,慢慢消化,以上就是全部内容!请务必掌握,创作不易,欢迎大家点赞加关注评论,您的支持是我前进最大的动力!下期再见!