认识UDP、TCP协议
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
一、Socket
首先我们需要了解一下socket。
在上一篇文章当中我们了解了TCP-IP五层协议模型初识网络IP、端口、网络协议、TCP-IP五层模型_革凡成圣211的博客-CSDN博客TCP/IP五层协议详解https://blog.csdn.net/weixin_56738054/article/details/128666970?spm=1001.2014.3001.5502
在这篇文章当中可以得知应用层是面向客户的层面。如果发起网络通信那么应用层需要向传输层发送应用层报文。
那么应用层如果想给传输层发送报文那么就一定需要调用操作系统提供的一些api那么建立起应用层和传输层之间的联系的这些api就是socket。严格意义上面来说这些socket的api属于传输层。
TCP、UPD协议就是socket的api提供的两种风格。
二、TCP、UDP协议的区别
UPDP协议概括一下就是以下几个特点:无连接、不可靠传输、面向数据报、全双工。
TCP协议概括一下就是以下几个特点:有连接、可靠传输、面向字节流、全双工。
有连接&无连接
这里想举一个比较形象的例子来说明:
发短信、发微信这一类的通信、我们通常视为无连接的网络通信。
原因:当我们发送一条消息的时候仅仅只负责发出无论接收方是否收得到、有无作出回应都会照样发送。不确保接收方是否接收到消息
而打电话这样的连接方式就是属于典型的有连接。为什么呢因为打电话的时候一定要双方都可以接听电话才可以互传信息。确保接收方会接收到消息
总结一下UDP协议当中发送方和接收方的运输层进程之间没有建立"握手"只负责把应用层的协议打包成UDP报文段然后发送不关注接收方是否能接收到需要发送的信息因此UDP协议被视为无连接的协议。
而TCP协议在开始传输数据之前需要经过三次握手确保接收方一定可以接收到消息因此TCP协议是有连接的协议
可靠传输&不可靠传输
所谓的可靠性与不可靠性实际上就是发送方是否得知接收方已经接收到需要发送的信息。
例如在微信QQ通话当中发送方仅仅负责发送但是它无法得知接收方是否已经接收到消息这种情况下面我们就认为协议是不可靠的。
如果发送消息的时候新增了"已读不回"等类似的功能确定接收方已经接收到发送方传输的内容说明协议是可靠的。
面向数据报&面向字节流
UDP协议是面向数据报的协议。
在上一篇文章当中我们提到了传输层协议是以"数据报"为基本单位进行传输的操作系统不会对消息进行拆分。也就是直接把应用层传输过来的报文打包成UPD数据段然后传输到网络层。
TCP协议是面向字节流的协议。
TCP把数据看成一个无结构但是有序的字节流。
当使用TCP协议进行传输的时候一条应用层消息可能会被操作系统分组成多个TCP报文。也就是一个完整的应用层传输过来的一句话有可能会被操作系统拆分成多个TCP数据段也有可能仅仅是一个数据段。
例如应用层打算发送Hi This is JIM这一条消息到传输层的时候有可能这一条消息被拆分成两个TCP段:
也有可能仅仅是一个TCP段
而对于UDP协议不会对消息进行拆分
全双工
一个通信通道可以双向传输既可以发送又可以接收
我们常见的高速公路一般都是双向六车道或者双向八车道这样的类型的。这个场景就可以理解为全双工同时支持发送和接收。
而半双工可以理解为不支持发送和接收信号同时进行。例如青藏铁路因为施工难度极大并且当时修建铁路的条件有限因此建立的场景为单向通行的。可以理解为"半双工"。
三、Java当中对于传输层的一些api
①DatagramSocket
在操作系统当中一切皆为文件。
使用这个类表示一个socket对象。在操作系统当中也是把这个socket当成一个文件来处理的。相当于进程的文件描述符表上面的某一项。
普通的文件对应的硬件设备为硬盘而socket文件对应的硬件设备为网卡。
一个socket对象就可以和另外一个主机进行通信了如果需要和多个主机通信就需要使用多个socket对象。
构造方法
DatagramSocket() | DatagramSocket(int port) |
如果没有指定端口左图那么系统会自动分配一个没有被占用的端口。
如果指定了端口右图那么就会把socket和对应的端口关联起来。
send/receive方法:
void send(DatagramPacket packet) | void receive(DatagramPacket packet) |
这两个方法分别代表socket发送、接收应用层的报文的方法。
其中需要发送/接收的DatagramPackett就是一个应用层报文。
close()方法
用于关闭文件描述符表项释放进程当中的文件描述符表项所占用的空间。
②DatagramPacket
表述udp当中传输的一个应用层报文。构造这个对象可以指定一些具体的数据进去。
构造方法:
对应方法 | 方法说明 |
DatagramPacket(byte[] buf,int length) | 把buf数组作为地址 |
DatagramPacket(byte[] buf,int offset,int length,SocketAddress) | 把buf数组作为地址并且指定了需要传输的目标主机IP和端口号 |
四、实现一个UDP客户端-服务端的代码
编写之前我们首先需要做一个假设此处的UDP的客户端是运行在客户的手中的也就是访问某个网站/app的普通用户。
而服务端是运行在程序员的电脑当中的
UDP服务端
首先需要明确服务端是需要做什么的。
步骤①读取客户端的响应
步骤②: 根据请求计算响应
步骤③: 把响应返回给客户端
需要指定的属性:DatagramSocket socket;用于为客户端提供socket来接收应用层传输来的应用层报文。
构造方法当中初始化socket对象并且指定本机当中需要建立通信的端口号。
需要注意的是应用层与传输层建立连接的时候一定要指定socket的端口号如果不指定选用无参数的构造方法那样就无法明确UDP与哪个需要联系的应用层端口建立联系无法正常通信。
/**
* udp服务端
* @author 25043
*/
public class UdpEchoServer {
/**
* 与应用层建立联系的datagramSocket对象
*/
private DatagramSocket datagramSocket;
/**
* 服务器当中一定要关联一个端口号
* 端口号@param port
* 异常@throws SocketException
*/
public UdpEchoServer(int port) throws SocketException {
//构造一个对象
//让这个socket和客户端主机当中的进程建立联系
//port就是对应客户端主机的进程id
datagramSocket=new DatagramSocket(port);
}
UDP服务端代码编写
步骤1、
创建应用层报文对象(DatagramSocket receiveSocket)并且在构造方法当中指定一个字节类型的数组来存储客户端发送来的应用层报文的信息。
DatagramPacket receivePacket=new DatagramPacket(new byte[4096],4096);
步骤2、
使用socket来接收这个报文(reveive方法)并且为这个字节类型的数组填充信息。其中下划线的部分是在reveive方法内部进行填充的。
datagramSocket.receive(receivePacket);
下面图解一下这个receive方法:
此处数据经过层层分用到达了udp传输层协议。
当应用程序调用receive方法的时候相当于执行到了内核udp相关的代码。
前面的文章当中提到了udp报文到达传输层的时候会把udp数据段当中的载荷(也就是去掉udp数据报头之后的内容)取出来取出来的是应用层的数据存放到byte数组当中。
需要注意的是如果此时服务端没有收到客户端发送过来的数据那么程序就会在receive方法处阻塞等待。这种阻塞等待类似于Scanner的等待就是"等待IO"。
步骤3、
截取receivePacket当中实际的应用层报文的实际字节数组的长度调用receivePacket.getData()方法
//截取到实际数据例如应用层发来的"hello"被存放在byte数组当中的时候可能仅仅占用了
//一点点的空间因此截取的实际长度为"hello"字节数组的长度
// packet.getLength()
//获取到数据报的实际长度部分
String request=new String(receivePacket.getData(),0, receivePacket.getLength());
步骤4、
调用process方法模拟客户端对报文作出响应。假设返回一个新的字符串(response)
//这里模拟一下回显服务器
String response=process(request);
public String process(String request){
return "udp服务端已经响应:"+request;
}
步骤5、
再次构造应用层报文对象(DatagramSocket responseSocket)在构造方法当中指定需要返回给客户端的信息,包括:
①response转化为的字节数组
②字节数组的实际长度
③应用层的IP地址、端口号信息避免客户端发错响应
//把需要回应的字符串转化为字节数组
byte[] receiveBytes=response.getBytes();
//获取到这个字节数组的长度
int receiveLength=receiveBytes.length;
//获取到对应客户端的IP和端口号(SocketAddress)
SocketAddress address =receivePacket.getSocketAddress();
//构造返回给客户端的socket对象
DatagramPacket responsePacket=new DatagramPacket(receiveBytes,receiveLength,address);
步骤6、
把responseSocket发送给客户端
//发送给应用层
datagramSocket.send(responsePacket);
步骤7、
启动服务端
由于此时还没有客户端给服务端发送数据因此服务端会在receive方法这里阻塞等待
此时指定服务端的端口号为9090。
public static void main(String[] args) throws IOException {
UdpEchoServer udpEchoServer=new UdpEchoServer(9090);
udpEchoServer.start();
}
整体服务端代码:
/**
* udp服务端
* @author 25043
*/
public class UdpEchoServer {
/**
* 与应用层建立联系的datagramSocket对象
*/
private DatagramSocket datagramSocket;
/**
* 服务器当中一定要关联一个端口号
* 端口号@param port
* 异常@throws SocketException
*/
public UdpEchoServer(int port) throws SocketException {
//构造一个对象
//让这个socket和客户端主机当中的进程建立联系
//port就是对应服务端主机的进程端口号
datagramSocket=new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
while (true){
//用于接收应用层的内容的字节数组一定要长度足够长
DatagramPacket receivePacket=new DatagramPacket(new byte[4096],4096);
datagramSocket.receive(receivePacket);
//截取到实际数据例如应用层发来的"hello"被存放在byte数组当中的时候可能仅仅占用了
//一点点的空间因此截取的实际长度为"hello"字节数组的长度
// packet.getLength()
//获取到数据报的实际长度部分
String request=new String(receivePacket.getData(),0, receivePacket.getLength());
//这里模拟一下回显服务器
String response=process(request);
//把需要回应的字符串转化为字节数组
byte[] receiveBytes=response.getBytes();
//获取到这个字节数组的长度
int receiveLength=receiveBytes.length;
//获取到对应客户端的IP和端口号(SocketAddress)
SocketAddress address =receivePacket.getSocketAddress();
//构造返回给客户端的socket对象
DatagramPacket responsePacket=new DatagramPacket(receiveBytes,receiveLength,address);
//发送客户端
datagramSocket.send(responsePacket);
//输出处理结果
System.out.println("客户端IP:"+
receivePacket.getAddress()+
";客户端端口:"
+receivePacket.getPort());
}
}
public String process(String request){
return "服务端已经响应:"+request;
}
}
启动服务端
可以看到服务端一直在reveive方法处阻塞等待等待客户端发送数据
UDP客户端
客户端主要负责的工作就是和服务端建立通信并且为服务端的receive方法内部输送数据(DatagramPacket)等待服务端的send方法发送数据(DatagramPacket)并且作出回应。
客户端的参数
在客户端服务器当中初始化socket对象。
既然要发送消息那么肯定需要知道服务端的端口号+Ip地址
/**
* UDP客户端
* @author 25043
*/
public class UdpEchoClient {
/**
*与服务端建立联系的socket
*/
private DatagramSocket socket;
/**
* 服务端的ip
*/
private String serverIp;
/**
* 服务端的端口号
*/
private int serverPort;
/**
* 指定服务器的ip以及端口
* 服务器的ip@param serverIp
* 服务器的端口i@param serverPort
* 抛出的异常@throws SocketException
*/
public UdpEchoClient(String serverIp,int serverPort) throws SocketException {
socket=new DatagramSocket();
this.serverIp=serverIp;
this.serverPort=serverPort;
}
}
如果客户端给服务端发送一次请求
那么源ip就是客户端ip源端口号就是客户端的端口
目的ip就是服务端的ip地址目的端口号就是服务端的端口号。
现在我们已知的情况就是:
由于此时客户端和服务端都运行在本机上面因此客户端和服务端的ip地址都是127.0.0.1。
启动服务端的的时候我们已经指定了socket的端口号为9090.那么服务端的进程端口号就是9090.
可以看到在这里的客户端没有指定端口号。
虽然没有指定但是我们还是可以了解到一些默认的信息:
一次通信当中涉及到的ip和端口号有两组。
源ip、源端口号。
目的ip、目的端口号。
在前面我们提到的知识当中如果没有指定端口号那么在初始化客户端的socket的时候会被操作系统默认绑定一个没有被其他进程占用的端口号。
为什么客户端不需要指定一个特定的端口号呢
原因就是如果客户端在编写代码的时候指定了一个端口号那么这个端口号如果此时被电脑上面的其他进程占用了就无法正常和服务端通信。
会抛出一个异常无法占用端口的异常。
既然客户端不用指定一个特定的端口号但是为什么服务端需要指定一个特定的端口号呢
客户端是不可控的。
客户端在实际的应用场景当中是运行在客户的电脑/手机当中的也许运行着许许多多的应用程序程序进程并且哪些进程运行多久这些完全取决于用户而不取决于程序员。
因此无法确定哪个客户端端口什么时间被占用也就不好指定客户端的端口号。
既然这样那就不如让操作系统随机为客户端分配一个空闲的进程端口号就好了。
而服务端是可控的。
服务端是运行在程序员的电脑当中的
因此服务端进程占用哪些端口这些是可以被程序员自己控制的程序员可以在编写服务端代码的时候手动控制端口的占用情况。
如果不设置服务端的端口那么操作系统会随机为服务端分配一个端口这样反而提高了程序员管理代码的难度。
启动客户端(start方法)
步骤1、
从控制台获取用户的输入
System.out.println("客户您好请输入您想向服务器发送的内容:");
String request=input.next();
步骤2、构造UDP请求
此处的DatagramPacket需要包含request转化的byte[]数组及其长度
还需要包含服务端的IP、端口号。
//构造UDP请求
//转化为字节数组
byte[] requestBytes=request.getBytes();
int length=requestBytes.length;
//指定服务端的Ip以及端口号
DatagramPacket requestPacket=new DatagramPacket(requestBytes,
length,
InetAddress.getByName(serverIp),serverPort);
步骤3、发送数据包DatagramPacket
到服务端的receive方法当中
//发送到服务端的receive方法当中
socket.send(requestPacket);
步骤4、构造DatagramPacket并且读取服务器的响应
此处将要初始化一定长度的空的字节数组然后传入到receive方法当中等待服务器为这个传入的数组填充内容。
//读取服务器的UDP响应
DatagramPacket responsePacket=new DatagramPacket(new byte[4096],4096);
//接收服务端的响应
socket.receive(responsePacket);
步骤5、构造响应字符串
//构造响应的字符串
String response=new String(responsePacket.getData(),0,responsePacket.getLength());
//输出响应的字符串
System.out.println(response);
下面图解一下客户端&服务端通信的整个流程
服务端先启动
客户端后启动
启动服务器
客户端启动
可以看到在启动了服务端、客户端之后客户端已经收到了服务端的响应了。