3次握手4次挥手
链路层 MTU 最大传输单元 1500 MISS最大分段
TCP 层 数据 tcp +Data
TCP+MISS+id2
D1<MISS
ip +TCP+D1+id2
tcp+D1+id2
tcp一段一段(D1)传 传成功了在传第二段(D2) 数据编号id1
包活计时器 2h 2h后没有对方确认收到 每75秒之后会发送探测包 发送10次 还是没有回应 则失败
UDP(用户数据报协议)-短信
只管发送,不确认对方是否接收到
将数据及源和目的封装成数据包中,不需要建立连接
每个数据包的大小限制在64K之内
因为无需连接,因此是不可靠协议
不需要建立连接,速度快
应用场景: 视频直播,游戏LOL
TCP(传输控制协议)
建立连接,形成传输数据的通道
在连接中进行大数据传输(数据大小不收限制)
通过三次握手完成连接,是可靠协议,安全送达
必须建立连接,效率会稍低
数据链路层 不能大于1500个字节 数据太多必须分片
我们知道,当应用层程序之间进行网络数据传输时,在发送端,数据会从应用层沿着协议栈向下传输,通过TCP/IP层,然后经由链路层发送出去,而在接收
端,则是相反的顺序,数据经由链路层接收,然后沿着协议栈向上传输,通过IP/TCP层,最后由应用层程序进行读取。
而在IP层往链路层传输数据的时候,往往会做一个分片的 *** 作,对于大多数链路层来讲,它都有一个最大传输单元(MTU),表示能够发送数据量的大小,它是由硬件决定的。比如以太网的MTU为1500字节。当IP层传输给链路层的数据量大于其MTU时,那么IP层就会将数据拆分为小于其链路层MTU的数据片,再传输给链路层进行发送 ,但是对于不同的传输层协议(TCP/UDP)来说,在IP层上,需不需要进行分片是不同的
TCP层的分片
对于TCP来说,它是尽量避免分片的,为什么?因为如果在IP层进行分片了话,如果其中的某片的数据丢失了,对于保证可靠性的TCP协议来说,会增大重传,数据包的机率,而且只能重传整个TCP分组(进行IP分片前的数据包),因为TCP层是不知道IP层进行分片的细节的,也不关心。
当TCP层进行TCP分组的重传后,还会直接影响到应用层程序的性能,特别是在应用程序使用阻塞IO进行读写的时候。要理解这点,首先我们要知道当
应用层程序往TCPIP协议栈写数据的时候都做了些什么事。
在应用层程序中,我们可以有自己的发送缓冲区,而TCP层本身也有自己的一个发送缓冲区,默认情况下一般是8k大小,可以通过SO_SNDBUF设置或读取。 当我们在应用层往TCP层写数据的时候,实际上是将应用层发送缓冲区的数据拷贝到TCP层的发送缓冲区中。当TCP层的发送缓冲区满或者网络空闲时,TCP层就 会将其缓冲区中的数据通过IP层传到链路层的发送队列中。如果TCP层的发送缓冲区满而且应用层的数据没有写完时,内核会将write系统调用挂起,并不返回给应用层程序,直到应用层的数据全部拷贝到TCP层的缓冲区中。而由于TCP层要保证数据包的可靠性,即数据包丢失时要进行重传,那么TCP层在往网络发送TCP分组后,需要在其发送缓冲区中暂时保存发出的TCP分组数据用于后续可能的重传。
在这样的前提下,如果IP对来自TCP层的数据进行了分片, 那么就有可能使得应用层程序一直在write系统调用处挂起等待,引起性能的下降。
TCP层如何避免IP层的分片
首先,我们先回顾下TCP建立连接的3次握手:
在这3次握手中,除了确认SYN分节外,通信的两端还进行协商了一个值,MSS,这个值用来告诉对方,能够发送的TCP分节的大小。这个值一般是取其链路层的MTU大小减去TCP头部大小和IP头部的大小。 MSS=MTU-TCP头部大小-IP头部大小 MTU的值可以通过询问链路层得知。当两端确认好MSS后进行通信,当TCP层往IP层传输数据时,如果TCP层缓冲区的大小大于MSS,那么TCP层都会将其发送缓冲区中的数据切分成MSS大小的分组进行传输,由于MSS是通过MTU减去TCP头部大小和IP头部的大小计算得出的,MSS肯定比MTU小,那么到IP层的时候就可以避免IP层的分片。
UDP层的分片
如果我们采用的是UDP协议而不是TCP协议呢?在IP层会不会进行分片?由于UDP是不需要保证可靠性的,那么它就不会保存发送的数据包,TCP之所以保存发送的数据包是因为要进行重传。所以UDP本身是没有像TCP一样的发送缓冲区的。这就导致了对UDP进行write系统调用的时候,实际上应用层的数据是直接传输到IP层,由于IP层本身也不会有缓冲区,数据就会直接写到链路层的输出队列中。在这种情况下,IP层会不会对来自UDP的数据进行分片呢?这个取决于UDP数据报的大小。如果UDP数据报的大小大于链路层的MTU,那么IP层就会直接进行分片,然后在发送到链路层的输出队列中,反之,则不会进行分片,直接加上IP头部发送到链路层的输出队列中。
TCP/UDP实验
看完了理论,让我们实践一把,看是否与以上的理论相符。
对于TCP来说,它是尽量避免分片的。假设我们这里要发送给TCP层的数据大小为2748个字节,这个大小是明显大于链路层的发送数据的大小的,在这个情况
下我们来看,对于来自TCP层的数据,IP会不会进行分片。
从第一张图看来,应用层的2748个字节在TCP层就进行了分段,分层了两个TCP段,一个1460字节,一个1288字节。那么到IP层的时候,自然就不会在进行分片了。
从第二张看出,在这两个TCP分段中,在序号3处,IP的头部字段 (Don ' t Fragment) 被设置了,用于告诉IP层不要对该数据进行分片。
而对于MSS大小的协商,我们可以从下面这张看到,下面的是TCP CLIENT发出的第一个SYN TCP分段:
对于UDP来说,假设我们要发送的一个UDP数据包大小为1600个字节,那么在实际上通过UDP/IP分发出去的时候,会不会进行分片呢 看如下的:
从上面的可以看出,我们发送的数据包的大小为1600字节(序号1处),在UDP层,长度为1608字节(序号2处),这里的8个字节是UDP的头部字段的长度, 到了IP层(序号3处),我们可以清楚的看到IP对UDP数据包进行了分片,一个大小为1480字节,一个为128字节
区别:
1IP分片产生的原因是网络层的MTU;TCP分段产生原因是MSS
2IP分片由网络层完成,也在网络层进行重组;TCP分段是在传输层完成,并在传输层进行重组 //透明性
3对于以太网,MSS为1460字节,而MUT往往会大于MSS
故采用TCP协议进行数据传输,是不会造成IP分片的。若数据过大,只会在传输层进行数据分段,到了IP层就不用分片。
而我们常提到的IP分片是由于UDP传输协议造成的,因为UDP传输协议并未限定传输数据报的大小。 服务程序最为关键的设计是并发服务模型,当前有以下几种典型的模型:
- 单进程服务,使用非阻塞IO
使用一个进程服务多个客户,通常与客户通信的套接字设置为非阻塞的,阻塞只发生在select()、poll()、epoll_wait()等系统调用上面。这是一种行之有效的单进程状态机式服务方式,已被广泛采用。
缺点是它无法利用SMP(对称多处理器)的优势,除非启动多个进程。此外,它尝试就绪的IO文件描述符后,立即从系统调用返回,这会导致大量的系统调用发生,尤其是在较慢的字节传输时。
select()本身的实现也是有局限的:能打开的文件描述符最多不能超过FD_SETSIZE,很容易耗尽;每次从select()返回的描述符组中扫描就绪的描述符需要时间,如果就绪的描述符在末尾时更是如此(epoll特别彻底修复了这个问题)。
- 多进程服务,使用阻塞IO
也称作 accept/fork 模型,每当有客户连线时产生一个新的进程为之服务。这种方式有时是必要的,比如可以通过 *** 作系统获得良好的内存保护,可以以不同的用户身份运行程序,可以让服务运行在不同的目录下面。但是它的缺点也很明显:进程比较占资源,进程切换开销太大,共享某些信息比较麻烦。Apache 13就使用了这种模型,MaxClients数很容易就可以达到。Socket套接字,是由系统提供用于网络通信的技术( *** 作系统给应用程序提供的一组API叫做Socket API),是基于TCP/IP协议的网络通信的基本 *** 作单元。基于Socket套接字的网络程序开发就是网络编程。
socket可以视为是应用层和传输层之间的通信桥梁;
传输层的核心协议有两种:TCP,UDP;socket API也有对应的两组,由于TCP和UDP协议差别很大,因此,这两组API差别也挺大。
分类:
Socket套接字主要针对传输层协议划分为如下三类:
流套接字:使用传输层TCP协议
TCP,即Transmission Control Protocol(传输控制协议),传输层协议;
TCP的特点:
有连接:像打电话,得先接通,才能交互数据;
可靠传输:传输过程中,发送方知道接收方有没有收到数据(打电话就是可靠传输);
面向字节流:以字节为单位进行传输(非常类似于文件 *** 作中的字节流);
全双工:一条链路,双向通信;
有接收缓冲区,也有发送缓冲区。
大小不限
对于字节流来说,可以简单的理解为,传输数据是基于IO流,流式数据的特征就是在IO流没有关闭的情况下,是无边界的数据,可以多次发送,也可以分开多次接收。
数据报套接字:使用传输层UDP协议
UDP,即User Datagram Protocol(用户数据报协议),传输层协议。
UDP的特点:
无连接:像发微信,不需要接通,直接就能发数据;
不可靠传输:传输过程中,发送方不知道接收方有没有收到数据(发微信就是不可靠传输);
面向数据报:以数据报为单位进行传输(一个数据报都会明确大小)一次发送/接收必须是一个完整的数据报,不能是半个,也不能是一个半;
全双工:一条链路,双向通信;
有接收缓冲区,无发送缓冲区;
大小受限:一次最多传输64k;
对于数据报来说,可以简单的理解为,传输数据是一块一块的,发送一块数据假如100个字节,必须一次发送,接收也必须一次接收100个字节,而不能分100次,每次接收1个字节。
原始套接字
原始套接字用于自定义传输层协议,用于读写内核没有处理的IP协议数据。
二、UDP数据报套接字编程
UDPSocket中,主要涉及到两类:DatagramSocket、DatagramPacket;
DatagramSocket API
DatagramSocket 创建了一个UDP版本的Socket对象,用于发送和接收UDP数据报,代表着 *** 作系统中的一个socket文件,( *** 作系统实现的功能–>)代表着网卡硬件设备的抽象体现。
DatagramSocket 构造方法:
方法签名 方法说明
DatagramSocket() 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口(一般用于客户端)
DatagramSocket(int port) 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务端)
DatagramSocket 方法:
方法签名 方法说明
void receive(DatagramPacket p) 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待)
void send(DatagramPacket p) 从此套接字发送数据报包(不会阻塞等待,直接发送)
void close() 关闭此数据报套接字
DatagramPacket API
代表了一个UDP数据报,是UDP Socket发送和接收的数据报,每次发送/接收数据报,都是在传输一个DatagramPacket对象。
DatagramPacket 构造方法:
方法签名 方法说明
DatagramPacket(byte[] buf, int length) 构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中,接收指定长度(第二个参数length)
DatagramPacket(byte[] buf, int offset, int length,SocketAddress address) 构造一个DatagramPacket以用来发送数据报,发送的数据为字节数组(第一个参数buf)中,从0到指定长度(第二个参数length)。address指定目的主机的IP和端口号
DatagramPacket 方法:
方法签名 方法说明
InetAddress getAddress() 从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址
int getPort() 从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号
byte[] getData() 获取数据报中的数据
构造UDP发送的数据报时,需要传入 SocketAddress ,该对象可以使用 InetSocketAddress 来创建。
InetSocketAddress API
InetSocketAddress ( SocketAddress 的子类 )构造方法:
方法签名 方法说明
InetSocketAddress(InetAddress addr, int port) 创建一个Socket地址,包含IP地址和端口号
示例1:写一个简单的客户端服务程序,回显服务(EchoSever)
在这里插入描述
构建Socket对象有很多失败的可能:
端口号已经被占用,同一个主机的两个程序不能有相同的端口号(这就好比两个人不能拥有相同的电话号码);
此处,多个进程不能绑定同一个端口号,但是一个进程可以绑定多个端口,(这就好比一个人可以拥有多个手机号),一个进程可以创建多个Socket对象,每个Socket都绑定自己的端口。
每个进程能够打开的文件个数是有上限的,如果进程之间已经打开了很多文件,就可能导致此时的Socket文件不能顺利打开;
在这里插入描述
这个长度不一定是1024,假设这里的UDP数据最长是1024,实际的数据可能不够1024
在这里插入描述
这里的参数不再是一个空的字节数组了,response是刚才根据请求计算的得到的响应,是非空的,DatagramPacket 里面的数据就是String response的数据。
responsegetBytes()length:这里拿到的是字节数组的长度(字节的个数),而responselength得到的是字符的长度。
五元组
一次通信是由5个核心信息描述的:源IP、 源端口、 目的IP、 目的端口、 协议类型。
站在客户端角度:
源IP:本机IP;
源端口:系统分配的端口;
目的IP:服务器的IP;
目的端口:服务器的端口;
协议类型:TCP;
站在服务器的角度:
源IP:服务器程序本机的IP;
源端口:服务器绑定的端口(此处手动指定了9090);
目的IP:包含在收到的数据报中(客户端的IP);
目的端口:包含在收到的数据报中(客户端的端口);
协议类型:UDP;发送步骤:
使用 DatagramSocket(int port) 建立socket(套间字)服务。
将数据打包到DatagramPacket中去
通过socket服务发送 (send()方法)
关闭资源
import javaioIOException; import javanet; public class Send { public static void main(String[] args) { DatagramSocket ds = null; //建立套间字udpsocket服务 try { ds = new DatagramSocket(8999); //实例化套间字,指定自己的port } catch (SocketException e) { Systemoutprintln("Cannot open port!"); Systemexit(1); } byte[] buf= "Hello, I am sender!"getBytes(); //数据 InetAddress destination = null ; try { destination = InetAddressgetByName("19216815"); //需要发送的地址 } catch (UnknownHostException e) { Systemoutprintln("Cannot open findhost!"); Systemexit(1); } DatagramPacket dp = new DatagramPacket(buf, buflength, destination , 10000); //打包到DatagramPacket类型中(DatagramSocket的send()方法接受此类,注意10000是接受地址的端口,不同于自己的端口!) try { dssend(dp); //发送数据 } catch (IOException e) { } dsclose(); } }
接收步骤:
使用 DatagramSocket(int port) 建立socket(套间字)服务。(我们注意到此服务即可以接收,又可以发送),port指定监视接受端口。
定义一个数据包(DatagramPacket),储存接收到的数据,使用其中的方法提取传送的内容
通过DatagramSocket 的receive方法将接受到的数据存入上面定义的包中
使用DatagramPacket的方法,提取数据。UDP协议的全称是用户数据报,在网络中它与TCP协议一样用于处理数据包。在OSI模型中,在第四层——传输层,处于IP协议的上一层。UDP有不提供数据报分组、组装和不能对数据包的排序的缺点,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。
为什么要使用UDP
在选择使用协议的时候,选择UDP必须要谨慎。在网络质量令人不十分满意的环境下,UDP协议数据包丢失会比较严重。但是由于UDP的特性:它不属于连接型协议,因而具有资源消耗小,处理速度快的优点,所以通常音频、视频和普通数据在传送时使用UDP较多,因为它们即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。比如我们聊天用的ICQ和OICQ就是使用的UDP协议。
在Java中 *** 纵UDP
使用位于JDK中Javanet包下的DatagramSocket和DatagramPacket类,可以非常方便地控制用户数据报文。
在描述它们之前,必须了解位于同一个位置的InetAddress类。InetAddress实现了Javaio Serializable接口,不允许继承。它用于描述和包装一个Internet IP地址,通过三个方法返回InetAddress实例:
getLocalhost():返回封装本地地址的实例。
getAllByName(String host):返回封装Host地址的InetAddress实例数组。
getByName(String host):返回一个封装Host地址的实例。其中,Host可以是域名或者是一个合法的IP地址。
DatagramSocket类用于创建接收和发送UDP的Socket实例。和Socket类依赖SocketImpl类一样,DatagramSocket类的实现也依靠专门为它设计的DatagramScoketImplFactory类。DatagramSocket类有3个构建器:
DatagramSocket():创建实例。这是个比较特殊的用法,通常用于客户端编程,它并没有特定监听的端口,仅仅使用一个临时的。
DatagramSocket(int port):创建实例,并固定监听Port端口的报文。
DatagramSocket(int port, InetAddress localAddr):这是个非常有用的构建器,当一台机器拥有多于一个IP地址的时候,由它创建的实例仅仅接收来自LocalAddr的报文。
值得注意的是,在创建DatagramSocket类实例时,如果端口已经被使用,会产生一个SocketException的异常抛出,并导致程序非法终止,这个异常应该注意捕获。DatagramSocket类最主要的方法有4个:
Receive(DatagramPacket d):接收数据报文到d中。receive方法产生一个“阻塞”。
Send(DatagramPacket d):发送报文d到目的地。
SetSoTimeout(int timeout):设置超时时间,单位为毫秒。
Close():关闭DatagramSocket。在应用程序退出的时候,通常会主动释放资源,关闭Socket,但是由于异常地退出可能造成资源无法回收。所以,应该在程序完成时,主动使用此方法关闭Socket,或在捕获到异常抛出后关闭Socket。
“阻塞”是一个专业名词,它会产生一个内部循环,使程序暂停在这个地方,直到一个条件触发。
DatagramPacket类用于处理报文,它将Byte数组、目标地址、目标端口等数据包装成报文或者将报文拆卸成Byte数组。应用程序在产生数据包是应该注意,TCP/IP规定数据报文大小最多包含65507个,通常主机接收548个字节,但大多数平台能够支持8192字节大小的报文。DatagramPacket类的构建器共有4个:
DatagramPacket(byte[] buf, int length, InetAddress addr, int port):从Buf数组中,取出Length长的数据创建数据包对象,目标是Addr地址,Port端口。
DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port):从Buf数组中,取出Offset开始的、Length长的数据创建数据包对象,目标是Addr地址,Port端口。
DatagramPacket(byte[] buf, int offset, int length):将数据包中从Offset开始、Length长的数据装进Buf数组。
DatagramPacket(byte[] buf, int length):将数据包中Length长的数据装进Buf数组。
DatagramPacket类最重要的方法就是getData()了,它从实例中取得报文的Byte数组编码。
★简单的实例说明
{接收数据的服务器}
byte[] buf = new byte[1000];
DatagramSocket ds = new DatagramSocket(12345);
//开始监视12345端口
DatagramPacket ip = new DatagramPacket(buf, buflength);
//创建接收数据报的实例
while (true)
{
dsreceive(ip);
//阻塞,直到收到数据报后将数据装入IP中
Systemoutprintln(new String(buf));
}
{发送数据的客户端}
InetAddress target = InetAddressgetByName(">
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)