传输层协议与 Socket API 网络编程
传输层协议与 Socket API 概述
传输层为应用层提供数据传输服务,其核心是通过操作系统提供的 Socket API 实现。传输层有两个核心协议 ——TCP 和 UDP,由于两者特性差异较大,Socket API 也为它们提供了两套不同的实现方式。
传输层主要有两个核心的协议,分别是TCP和UDP
这两个协议的差别比较大,所以在进行编写代码的时候也是不同的风格。
为了让这两个协议都能够使用,因此socketapi提供了两套
TCP 与 UDP 协议特性对比
提够给TCP的具有以下特点:有连接,可靠传输,面向字节流,全双工
提够给TCP的具有以下特点:无连接,不可靠传输,面向数据报,全双工
有连接/无连接
这里的连接指的并不是物理上的连接,而是虚拟的,逻辑上的连接。
对于TCP协议来说,在TCP协议中,就保存了对端的信息。
比如我们现在让A和B通信,就会让A和B先建立连接,
这个时候A就保存了B的相关信息,B也保存了A的相关信息(也就是让彼此之间知道,谁是和他建立连接的那一个)
像这种方式就是“有连接”
而对于UDP来说,UDP协议本身并不保存对方的信息。这种方式就叫做“无连接”。
但是我们也可以在代码中手动保存对方的信息,但这样的操作并不是UDP本身的行为。
可靠传输/不可靠传输
我们要知道的是在网络上传输数据的时候,是很容易出现数据丢失的情况的(这个情况也叫做丢包)
丢包通常会在以下两种情况发生:
1.网络上传输的数据实际上是光信号/电信号这种物理媒介,这些媒介都可能受到外界的干扰。当他们受到干扰的时候,本来传输的是0101,但是其中有些bit位就被修改了。
此时这样的混乱的数据就会被识别出来,于是就把这些数据给丢弃了。
2.网络世界是由路由器和交换机组成的,这俩就类似于现实生活中的“十字路口”。
我们经常在十字路口出现“堵车”的情况,
对应到网络中就是在某个时间点我们实际要转发的数据量超过了设备能转发的上限。
也就是说,一个数据包发送之后,并不能百分百确定就到达对方。
可靠传输的意思并不是确保数据包能到达对方,而是在传输的过程中尽可能提高传输的效率。
而不可靠传输只是把数据包发送出去,后续的过程就不在追踪和管理。
面向字节流/面向数据报
面向字节流在读写数据的时候是以字节为单位的;
这种方式的读取方式支持读取任意长度的数据,但是会存在粘包问题
而面向数据报在读写数据的时候是以一个数据报为单位的(并不是字符)
这种方式一次必须读写一个UDP数据报,不能是半个。
全双工/半双工
全双工是指在一个通信链路中支持双向通信(能读也能写)
而半双工是指在一个通信链路中只支持单向通信(要么读,要么写)
使用 socket api进行网络编程
我们先来讲一下UDP的socket api
这里面涉及到了两个核心的类:DatagramSocket和DatagramPacket
网卡在系统中被抽象成为了socket文件
DatagramSocket
DatagramSocket就是用来表示网卡的文件,我们通过它来操作网卡。
它抽象表示网卡的操作接口,用于发送和接收数据。
我们直接操作网卡并不好操作,但是我们可以把网卡抽象为socket文件,这个socket文件就相当于是“网卡的遥控器”
下面我们来看一下这个类的构造方法:
构造方法的功能就相当于是在打开文件。
第二个构造方法的port实际上就是传入一个端口号
下面我们再来看一下读写操作对应的方法:
receive就是读取操作,send就相当于是写操作
这两个方法的参数就是DatagramPacket 而DatagramPacket就是用来表示一个完整的UDP数据报的
下面这个方法就是用来关闭文件:
DatagramPacket
它表示一个完整的 UDP 数据报,包含数据内容和地址信息
UDP的载荷数据就可以通过上面的构造方法来设定
回显服务器的工作原理是:客户端向服务器发送数据(请求),服务器会将接收到的数据原封不动地返回(响应)。
简单来说,服务器返回的响应内容与客户端发送的请求内容完全一致。
客户端代码实现
下面我们用代码来实现一下:
由于服务器并不知道什么时候客户端会发送请求,所以服务器内会有一个死循环来处理客户端发送的请求:
public void start(){while (true){
// 1.读取请求并解析// 2.根据请求来计算响应(这也是服务器中最关键的逻辑)// 3.把相应的反应返回给客户端}}
读取请求并解析
// 1.读取请求并解析
// DatagramPacket 表示一个 UDP 数据报. 此处传入的字节数组, 就保存 UDP 的载荷部分.DatagramPacket request=new DatagramPacket(new byte[4096],4096);socket.receive(request);
这里的receive方法接收的是一个输出型参数request。当request作为输出参数传入receive方法后,由于该请求数据报已包含源IP、源端口等信息,待方法执行完毕时,我们就能获得一个包含完整源地址信息的UDP数据报。
接下来我们要把拿到的二进制数据转化为字符串
// 接下来我们需要把读到的二进制数据转换为字符串 getData()是拿到当前数据包中的字节数组String request1=new String(request.getData(),0, request.getLength());
根据请求来计算响应
String response = process(request);
public String process(String request) {return request;}
把响应返回给客户端
// 我们需要根据 response 构造 DatagramPacket, 发送给客户端.
// request.getSocketAddress()是为了得到源IP和源端口 因为UDP协议自身没有保存对方的信息(不知道发给谁) 所以我们需要指定 目的 ip 和 目的端口.DatagramPacket responsePacket=new DatagramPacket(response.getBytes(),response.getBytes().length,request.getSocketAddress());
这里的newDatagramPacket的意义在于构造出一个响应数据报
接下来我们需要把构造好的相应数据报发送出去
socket.send(responsePacket);
客户端代码实现
准备工作
private DatagramSocket socket=null;
// 由于UDP并不保存对端的信息所以需要我们用代码保存private String serverIP;private int serverPort;public UdpEchoClient(String serverIP, int serverPort) throws SocketException {this.serverIP = serverIP;this.serverPort = serverPort;socket=new DatagramSocket();}
这里的socket=new DatagramSocket()中的源端口应该是让操作系统随机分配一个空闲的端口
如果客户端的端口是固定的端口,那么很有可能客户端在运行的时候这个端口已经被别的程序所占用,于是就会使得当前程序无法运行。
从控制台读取用户输入的内容并解析
while(true){//1.先读取到内容System.out.println("请输入要发送的内容");if(!scanner.hasNext()){break;}String request=scanner.next();
把请求发送到服务器
// 2.接下来我们需要把我们的请求发送给服务器,就需要构建DatagramSocket对象
// 构造的过程中我们不光要构造载荷,还需要设置服务器的IP和服务器的端口号DatagramPacket requestPacket=new DatagramPacket(request.getBytes(),request.getBytes().length,InetAddress.getByName(serverIP),serverPort);
发送数据报
// 3.接下来就开始发送数据报socket.send(requestPacket);
接受服务器的响应
// 4.接下来我们需要接受服务器的响应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);