计算机网络 - TCP 与 UDP

本文介绍了TCP和UDP两种传输层协议,包括TCP的三次握手连接建立、连接终止,以及TCP和UDP的主要区别。此外,还详细讲解了Socket编程,包括Socket API的使用、Select模型的工作原理和应用,并提供了TCP Server、Client、UDP Server和Client的编程示例。

TCP 与 UDP 介绍

TCP

TCP(Transmission Control Protocol 传输控制协议)是一种 面向连接的、可靠的、基于字节流 的传输层通信协议,由 IETF 的 RFC 793 定义。在简化的计算机网络 OSI 模型中,它完成第四层传输层所指定的功能,用户数据报协议(UDP)是同一层内另一个重要的传输协议。在因特网协议族(Internet protocol suite)中,TCP 层是位于 IP 层之上,应用层之下的中间层。不同主机的应用层之间经常需要可靠的、像管道一样的连接,但是 IP 层不提供这样的流机制,而是提供不可靠的包交换。

应用层向 TCP 层发送用于网间传输的、用 8 位字节表示的数据流,然后 TCP 把数据流分区成适当长度的报文段(通常受该计算机连接的网络的数据链路层的最大传输单元(MTU)的限制)。之后 TCP 把结果包传给 IP 层,由它来通过网络将包传送给接收端实体的 TCP 层。TCP 为了保证不发生丢包,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的包发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据包就被假设为已丢失将会被进行重传。TCP 用一个校验和函数来检验数据是否有错误;在发送和接收时都要计算校验和。

在拥塞控制上,采用广受好评的 TCP 拥塞控制算法(也称 AIMD 算法)。该算法主要包括三个主要部分:1)加性增、乘性减;2)慢启动;3)对超时事件做出反应。

连接建立

TCP 是因特网中的传输层协议,使用三次握手协议建立连接。当主动方发出 SYN 连接请求后,等待对方回答 SYN+ACK 并最终对对方的 SYN 执行 ACK 确认。这种建立连接的方法可以防止产生错误的连接,TCP 使用的流量控制协议是可变大小的滑动窗口协议。
TCP 三次握手的过程如下:

  1. 客户端发送 SYN(SEQ=x) 报文给服务器端,进入 SYN_SEND 状态。
  2. 服务器端收到 SYN 报文,回应一个 SYN(SEQ=y) ACK(ACK=x+1)报文,进入 SYN_RECV 状态。
  3. 客户端收到服务器端的 SYN 报文,回应一个 ACK(ACK=y+1)报文,进入 Established 状态。

三次握手完成,TCP 客户端和服务器端成功地建立连接,可以开始传输数据了。

在这里插入图片描述

连接终止

建立一个连接需要三次握手,而终止一个连接要经过四次握手,这是由 TCP 的半关闭(half-close)造成的。具体过程如下图所示。

  1. 某个应用进程首先调用 close,称该端执行“主动关闭”(active close)。该端的 TCP 于是发送一个 FIN 分节,表示数据发送完毕。
  2. 接收到这个 FIN 的对端执行 “被动关闭”(passive close),这个 FIN 由 TCP 确认。
    注意:FIN 的接收也作为一个文件结束符(end-of-file)传递给接收端应用进程,放在已排队等候该应用进程接收的任何其他数据之后,因为,FIN 的接收意味着接收端应用进程在相应连接上再无额外数据可接收。
  3. 一段时间后,接收到这个文件结束符的应用进程将调用 close 关闭它的套接字。这导致它的 TCP 也发送一个 FIN。
  4. 接收这个最终 FIN 的原发送端 TCP(即执行主动关闭的那一端)确认这个 FIN。
    既然每个方向都需要一个 FIN 和一个 ACK,因此通常需要 4 个分节。

在这里插入图片描述

UDP

UDP 是 User Datagram Protocol(用户数据报协议)的简称,是 OSI(Open System Interconnection,开放式系统互联) 参考模型中一种 无连接 的传输层协议,提供面向事务的简单不可靠信息传送服务。

UDP 协议全称是用户数据报协议,在网络中它与 TCP 协议一样用于处理数据包,是一种无连接的协议。在 OSI 模型中,在第四层——传输层,处于 IP 协议的上一层。UDP 有 不提供 数据包分组、组装和不能对数据包进行排序的缺点,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。UDP 用来支持那些需要在计算机之间传输数据的网络应用。包括网络视频会议系统在内的众多的客户/服务器模式的网络应用都需要使用 UDP 协议。UDP 协议从问世至今已经被使用了很多年,虽然其最初的光彩已经被一些类似协议所掩盖,但是即使是在今天 UDP 仍然不失为一项非常实用和可行的网络传输层协议。

与所熟知的 TCP(传输控制协议)协议一样,UDP 协议直接位于 IP(网际协议)协议的顶层。根据 OSI(开放系统互连)参考模型,UDP 和 TCP 都属于传输层协议。UDP 协议的主要作用是将网络数据流量压缩成数据包的形式。一个典型的数据包就是一个二进制数据的传输单位。每一个数据包的前 8 个字节用来包含报头信息,剩余字节则用来包含具体的传输数据。

UDP 是 OSI 参考模型中一种无连接的传输层协议,它主要用于 不要求分组顺序到达 的传输中,分组传输顺序的检查与排序由应用层完成,提供 面向事务的简单不可靠信息传送服务 。UDP 协议基本上是 IP 协议与上层协议的接口。UDP 协议适用端口分别运行在同一台设备上的多个应用程序。

总结

  1. TCP与UDP区别总结:

    1. TCP 面向连接(如打电话要先拨号建立连接); UDP 是无连接的,即发送数据之前不需要建立连接

    2. TCP 提供可靠的服务。也就是说,通过 TCP 连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP 尽最大努力交付,即不保证可靠交付,TCP 通过校验和,重传控制,序号标识,滑动窗口、确认应答实现可靠传输。如丢包时的重发控制,还可以对次序乱掉的分包进行顺序控制。

    3. UDP 具有较好的实时性,工作效率比 TCP 高,适用于对高速传输和实时性有较高的通信或广播通信。

    4. 每一条 TCP 连接只能是点到点的;UDP 支持一对一,一对多,多对一和多对多的交互通信

    5. TCP 对系统资源要求较多,UDP 对系统资源要求较少。

  2. 为什么 UDP 有时比 TCP 更有优势?

    1. UDP 以其简单、传输快的优势,在越来越多场景下取代了 TCP,如实时游戏。

    2. 网速的提升给 UDP 的稳定性提供可靠网络保障,丢包率很低,如果使用应用层重传,能够确保传输的可靠性。

    3. TCP 为了实现网络通信的可靠性,使用了复杂的拥塞控制算法,建立了繁琐的握手过程,由于 TCP 内置的系统协议栈中,极难对其进行改进。

    4. 采用 TCP,一旦发生丢包,TCP 会将后续的包缓存起来,等前面的包重传并接收到后再继续发送,延时会越来越大,基于 UDP 对实时性要求较为严格的情况下,采用自定义重传机制,能够把丢包产生的延迟降到最低,尽量减少网络问题对游戏性造成影响。

报文

TCP 报文段头

TCP 头

UDP 报文头

UDP 头

使用 Socket 编程

套接字是通信端点的抽象,其英文 socket,即为插座,孔的意思。如果两个机子要通信,中间要通过一条线,这条线的两端要连接通信的双方,这条线在每一台机子上的接入点则为 socket,即为插孔,所以在通信前,我们在通信的两端必须要建立好这个插孔,同时为了保证通信的正确,端和端之间的插孔必须要一一对应,这样两端便可以正确的进行通信了,而这个插孔对应到我们实际的操作系统中,就是 socket 文件,我们再创建它之后,就会得到一个操作系统返回的对于该文件的描述符,然后应用程序可以通过使用套接字描述符访问套接字,向其写入输入,读出数据。

站在更贴近系统的层级去看,两个机器间的通信方式,无非是要通过运输层的TCP/UDP,网络层IP,因此socket本质是编程接口(API),对TCP/UDP/IP的封装,TCP/UDP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口。

1、Socket 的创建

#include <sys/socket.h>
int socket (int domain, int type, int protocol);
//
int server_sockfd = socket(AF_INET, SOCK_STREAM, 0);

这样,我们便创建了一个socket,对于socket接收的参数都有什么意义呢?从上面,我们可以知道socket是对于底层网络通信的一个封装,而对于底层的网络通信也是具备多种类型的。而这些参数则是通过组合来表示各类通信的特征,从而建立正确的套接字。

  • domain: 通信的特性,每个域有自己的地址表示格式,AF打头,表示地址族(Address family)

domain

  • type:套接字的类型,进一步确定通信特征。

type

  • protocol: 表示为给定域和套接字类型选择默认协议,当对同一域和套接字类型支持多个协议时,可以通过该字段来选择一个特定协议,通常默认为 0。上面设置的 socket 类型,在执行的时候也会有默认的协议类型提供,比如 SOCK_STREAM 就 TCP 协议。
    • SOCK_STREAM 这种是 Transmission Control Protocol 传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议,即每次收发数据之前必须通过 connect 建立连接
    • SOCK_DGRAM 这种是 User Datagram Protocol 协议的网络通讯,是一种 无连接 的传输层协议,提供面向事务的简单 不可靠 信息传送服务。
    • SOCK_RAW 套接字提供一个数据报接口。通过这个我们可以直接访问下面的网络层,绕过 TCP/UDP,因此我们可以进行制定自己的传输层协议。

protocol
2、Socket 的关闭

  • 当我们不再使用 Socket 的时候,我们可以调用 close 函数来将其关闭,释放该文件描述符,这样便可以得到重新的使用。
  • 套接字通信是双向的,但是,我们可以采用 shutdown 函数来禁止一个套接字的 I/O。
#include<sys/socket.h>
int shutdown(int sockfd, int how);

how 可以用来指定读端口或者是写端口,这样我们便可以关闭掉读端或者写端。

3、字节序

字节序是处理器架构的特性,用来指示像整数这种数据类型的内部如何排序,大端和小端,因此如果通信双方的处理器架构不同,则会导致字节序的不一致的问题出现。最底层的网络协议指定了字节序,大端字节序,但是应用程序在处理数据时,则会遇到字节序不一致的问题。对此,系统提供了进行处理器字节序和网络字节序之间实施转换的函数。

#include <arpa/inet.h>

uint32_t htonl(uint32_t hostint32)//主机字节转化为网络字节序
uint16_t htons(uint16_t hostint16)
uint32_t ntohl(uint32_t netint32)//网络字节序转化为主机字节序
unint16_t ntohs(uint16_t netint16)

4、地址格式

如何表示一个要通信的进程,需要一个网络地址和端口,而在系统中如何具体的标示这一特征呢?根据之前 socket 的创建,我们知道不同socket 对应了不同的通信特征,而对于不同的通信特征,其地址表示上也有一些差别。
这里我们只看一下 IPV4 因特网域地址的表示结构。

struct sockaddr_in {
   
    
	sa_family_t sin_family; 
	in_port_t sin_port; 
	struct in_addr sin_addr;
}
  • sin_family: 通信的的域,这里为 AF_INET: IPV4 因特网域
  • sin_port: 通信的端口
  • sin_addr: 网络地址
/** 255.255.255.255 */
#define IPADDR_NONE         ((u32_t)0xffffffffUL)
/** 127.0.0.1 */
#define IPADDR_LOOPBACK     ((u32_t)0x7f000001UL)
/** 0.0.0.0 */
#define IPADDR_ANY          ((u32_t)0x00000000UL)
/** 255.255.255.255 */
#define IPADDR_BROADCAST    ((u32_t)0xffffffffUL)

5、socket 选项设置

对于 Socket,系统提供了更具体细致化的一些配置选项,通过这些配置选项,我们可以进行进一步具体的配置。

#include <sys/socket.h>

int setsockopt(int sockfd, int level, int option, const void *val, socklen_t len);
  • sockfd: 我们要进行配置的 socket
  • level: 根据我们选用的协议,配置相应的协议编号
  • option: 选项即为下表
  • val: 用来存放返回值

在这里插入图片描述

Socket API

#if LWIP_COMPAT_SOCKETS
#define accept(a,b,c)         lwip_accept(a,b,c)
#define bind(a,b,c)           lwip_bind(a,b,c)
#define shutdown(a,b)         lwip_shutdown(a,b)
#define closesocket(s)        lwip_close(s)
#define connect(a,b,c)        lwip_connect(a,b,c)
#define getsockname(a,b,c)    lwip_getsockname(a,b,c)
#define getpeername(a,b,c)    lwip_getpeername(a,b,c)
#define setsockopt(a,b,c,d,e) lwip_setsockopt(a,b,c,d,e)
#define getsockopt(a,b,c,d,e) lwip_getsockopt(a,b,c,d,e)
#define listen(a,b)           lwip_listen(a,b)
#define recv(a,b,c,d)         lwip_recv(a,b,c,d)
#define recvfrom(a,b,c,d,e,f) lwip_recvfrom(a,b,c,d,e,f)
#define send(a,b,c,d)         lwip_send(a,b,c,d)
#define sendto(a,b,c,d,e,f)   lwip_sendto(a,b,c,d,e,f)
#define socket(a,b,c)         lwip_socket(a,b,c)
#define select(a,b,c,d,e)     lwip_select(a,b,c,d,e)
#define ioctlsocket(a,b,c)    lwip_ioctl(a,b,c)
 
#if LWIP_POSIX_SOCKETS_IO_NAMES
#define read(a,b,c)           lwip_read(a,b,c)
#define write(a,b,c)          lwip_write(a,b,c)
#define close(s)              lwip_close(s)

socket() --得到文件描述符!
bind() --我们在哪个端口?
connect() --Hello!
listen() --有人给我打电话吗?
accept() --“Thank you for calling port 3490.”
send() 和 recv() --Talk to me, baby!
sendto() 和 recvfrom() --Talk to me, DGRAM-style
close() 和 shutdown() --滚开!
getpeername() --你是谁?
gethostname() --我是谁?
DNS --你说“白宫”,我说 “198.137.240.100”

Select

select 系统调用的的用途是:在一段指定的时间内,监听用户感兴趣的文件描述符上可读、可写和异常等事件。

1、阻塞模式

int iResult = recv(s, buffer,1024);

这是用来接收数据的,在默认的阻塞模式下的套接字里,recv会阻塞在那里,直到套接字连接上有数据可读,把数据读到buffer里后recv函数才会返回,不然就会一直阻塞在那里。在单线程的程序里出现这种情况会导致主线程(单线程程序里只有一个默认的主线程)被阻塞,这样整个程序被锁死在这里,如果永 远没数据发送过来,那么程序就会被永远锁死。这个问题可以用多线程解决,但是在有多个套接字连接的情况下,这不是一个好的选择,扩展性很差。

2、非阻塞模式

int iResult = ioctlsocket(s, FIOBIO, (unsigned long *)&ul);
iResult = recv(s, buffer,1024);

这一次recv的调用不管套接字连接上有没有数据可以接收都会马上返回。原因就在于我们用ioctlsocket把套接字设置为非阻塞模式了。不过你跟踪一下就会发现,在没有数据的情况下,recv确实是马上返回了,但是也返回了一个错误:WSAEWOULDBLOCK,意思就是请求的操作没有成功完成。看到这里很多人可能会说,那么就重复调用recv并检查返回值,直到成功为止,但是这样做效率很成问题,开销太大。

select模型的出现就是为了解决上述问题。
在这里插入图片描述
如上所示,用户首先将需要进行 IO 操作的 socket 添加到 select 中,然后阻塞等待select系统调用返回。当数据到达时,socket 被激活,select 函数返回。用户线程正式发起 read 请求,读取数据并继续执行。

从流程上来看,使用 select 函数进行 IO 请求和同步阻塞模型没有太大的区别,甚至还多了添加监视 socket,以及调用 select 函数的额外操作,效率更差。但是,使用 select以后最大的优势是

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值