协议是我们接触互联网以来,听到的频率较高的一个术语,那它具体指的是什么呢?协议,是网络协议的简称,意思是在网络通信的过程中,所经过的网络设备必须要共同遵守一组约定,准则。举个例子来说,如果A君要去相亲,会提前和相亲对象说好,在某某咖啡厅,信物是志摩的诗夹带一束玫瑰。那么只要到达咖啡厅,见到拿着信物的人,那么就是A君的相亲对象了。
但是实际上在网络通信中,协议是非常复杂的,不同的环境就会有不同的协议。如果协议过于复杂需要对其进行拆分,将拆分好的协议在进行分类同时根据这些不同的类别进行分层。故就有要求:上层协议调用下层协议;下层协议给上层提供支持,不能跨层调用。
在每一次的网络数据传输中,都会存在发送端和接收端。
发送端:数据的发送方进程,称为发送端,发送端主机就是网络通信的源主机
接收端:数据的接收方进程,称为接收端,接收端主机即网络通信中的目的主机
根据上面的描述同样也会存在请求和响应,服务端与客户端,客户端发来请求,服务端根据请求解析返回响应。
对于网络分层,当下最为广泛的分层是TCP/IP五层网络模型,即应用层、传输层、网络层、数据链路层、物理层;这里不做太多赘述,我们想要实现网络程序关注的是应用层和传输层。当我们编写一个网络程序时,需要上层协议调用下层协议,此时应用层要调用传输层,传输层给应用层提供一组API,统称为 Socket API。
Socket 套接字是基于TCP/IP协议的网络通信的基本单元,是由系统提供用于网络通信的技术,所以认为基于Socket套接字的网络程序开发就是网络编程。
Socket也提供了2种不同的API,一种是流套接字:传输层TCP协议,一种是数据报套接字:使用传输层UDP协议。
我们本次先尝试学习数据报套接字,UDP协议。UDP就是User Datagram Protocol(用户数据协议),传输层协议,我们可以将数据报理解为一段语音,一条消息,通过网络介质将其传输,让请求方和接收方形成通信。
基于UDP协议的网络编程,存在2个API,一个是 DatagramSocket API ,另一个就是DatagramPacket API,逐一给大家介绍。
DatagramSocket是UDP Socket,是用于发送和接收UDP数据报。Datagram就是数据报,Socket则是说明这个对象是一个Socket对象,Socket对象是一个特殊的文件,是对应到网卡这个硬件设备上,和普通的文件(execl、ppt、world)不是一回事。我们如果想要进行网络通信,就必须要有Socket这样的对象,间接的去操作网卡;我们可以做如下区别。
往Socket对象中写数据,相当于通过网卡发送消息。
从Socket对象中读数据,相当于通过网卡接收数据。
DatagramSocket 是UDP Socket,用于发送和接收UDP数据报,构造方法有2个。
方法签名 | 方法说明 |
DatagramSocket() | 创建一个UDP数据报套接字Socket,绑定一个随机端口(客户端) |
DatagramSocket(int port) | 创建一个UDP数据报套接字Socket,绑定本机的指定端口(服务端) |
注意:2个Socket方法,一个绑定随机端口是给客户端使用,一个却需要指定端口给服务器使用,这其中是有一定的说法。
举个例子
服务器相当于是一个面馆,不久前A君租了一个门面房,准备开一间陕西臊子面馆,于是印了传单,打了广告。传单上印好了地址,面馆位于光明路1688号LG层101室,门店的地址一定是提前就确认好的。当顾客来点餐,A君说,你先找个地方坐好,比方说顾客坐在C10d的位置,臊子面做好我给你端过去。
过了几天当这个顾客再来吃面的时候,面馆的位置肯定是没有变化的,还是在光明路1688号LG层101室,但是原先C10这个位置就不一定是空出来的,有可能有其他人坐在这个位置上。
所以,面馆作为服务器,位置(端口)一定是不能随意变的,需要指定,但是座位就不一样,只要是空的座位,你就可以坐下来点餐。
DatagramSocket 方法:
方法签名 | 方法说明 |
void receive(DatagramPacket p) | 从此套接字接收数据报(如果没有接收到数据报,就会阻塞等待) |
void send(DatagramPacket p) | 从此套接字发送数据报(不会阻塞等待,直接发送) |
void close() | 关闭此数据报套接字 |
receive:接收数据报
send:发送数据报
close:关闭数据报,上面说到Socket也是一种文件,如果用完不关闭,会造成文件资源泄露的问题。
DatagramPacket是UDP Socket发送和接收的数据报
DatagramPacket构造方法:
方法签名 | 方法说明 |
1.DatagramPacket( byte[]buf, int length) | 构造一个DatagramPacket用来接收数据报,接收的数据保存在字节数组(第一个参数buf中),接收指定长度 (第二个参数length) |
2.DatagramPacket(byte[]buf,int offset,int length,SocketAddress address) | 构造一个DatagramPacket用来发送数据报,发送的数据为字节数组(第一个参数buf)中,从0到指定长度(第二个参数length)。 address指定目的主机的IP和端口号 |
注意:第一个版本不需要设置地址进去,通常用来接收消息。第二个版本需要设置地址进去,通常用来发送消息。
DatagramPackt方法:
方法签名 | 方法说明 |
InetAddress getAddress() |
从接收的数据中,获取发送端主机IP地址;或从发送的数据中,获取接收端主机IP |
int getPort() | 从接收的数据中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号 |
byte[] getData() | 获取数据报中的数据 |
注意:构造UDP发送的数据时,需要传入SocketAddress,这个对象可以使InetSocketAddress来创建
DatagramSocket和DatagramPacket的区别
socket是文件,是点餐的话筒,通过这个话筒来对话
packet是要传输的数据,是端上来的一碗面
通过上述对Sockt API的了解,我们写一个简单服务端以及客户端的回显服务器,即客户端发送了一个请求,服务器返回一个一模一样的响应。类似一个复读机,我喊的是什么返回的就是什么。
一个服务器,主要做要做三个核心任务
注意: 客户端和服务端是成对出现的
我们可以将建立Socket理解为创建一个遥控器️来操控网卡,毕竟是通过网卡来发送和接收数据。
public class UdpEchoServer{ //服务器
private DatagramSocket socket = null; //只有通过Socket对象,才能间接的操作对象
}
绑定了端口不一定能成功,如果我们需要绑定的端口已经被占用,此时绑定的端口就会出错,在统一时刻,同一个主机和端口只能被一个进程绑定(一碗面只能被点单的人吃掉)。
public UdpEchoServer (int port) throws SocketExeption{
//构造Socket的同时需要绑定端口
socket = new DatagramSocket(port);//port是端口的意思
}
在确保Socket和绑定端口成功后,开始启动服务器,这也是最核心的任务。
我们已经说过一个服务器主要任务有三件事,这是每一个服务器执行任务的逻辑,是一个死循环,所以需要一个while。因为服务器并不是只是用一次,一家面馆不可能只为一个顾客服务,一次服务结束了还有其他顾客,面馆里面也不可能只有一个顾客来吃饭。
public void start(){
System.out.println("服务器启动");//提示
while(true){
//每次循环,要做三件事
//1.读取并请求解析
//2.根据请求计算响应(本次省略这个步骤)
//3.把响应结果写回到客户端
}
}
1.请求并解析
比方说,面馆来了顾客,顾客说我要一碗臊子面,我们收到了请求就开始解析,这个时候就要用到socket.receive()方法;这个方法的参数类型是输出类型参数。
输出型参数是什么意思呢,一般来说我们都是以参数来作为方法的"输入",用返回值作为方法的"输出"。这里的输入输出并不是键盘上的输入和输出。所谓输出型参数可以理解为去学校的食堂吃饭,但是我们并没用食堂的餐具,我们带着自己的空饭盒去打饭,阿姨会根据你想要吃哪些东西,把你的饭盒装满饭菜。receive就是类似于自己带着饭盒去打饭。
既然receive是输出型参数,饭盒也不会凭空出现,这个时候就需要我们去构造一个空的饭盒。
public void start(){
System.out.println("服务器启动");//提示
while(true){
//每次循环,要做三件事
//1.读取并请求解析
//构造一个空的饭盒方便打饭
DatagramPacket requestPacket = new DatagramPacket(new byte[1080],1080);
socket.receive(requestPacket);
}
}
空的饭盒构造好了,大小为1080,并没有实际的意义只是一个空饭盒。
我们构造出的 requestPacket 这里装的就是客户端的请求,这个饭哪里来的呢?从阿姨手上,也就是从网卡上来的。此时通过receive执行之后,就可以把读到的数据放到参数中了。
问:如果我们带着空饭盒去打饭,但是饭没好怎么办?
答:一旦服务器启动,调用start()方法,就会立即执行到receive这里,如果客户端此时没有发来请求,receive就会阻塞等待,阿姨只能看着你的空饭盒,直到后厨把饭做好才能打饭给你。
除了以上的请求我们还需要对requestPacket中的字节数组进行改造,如果客户端发来一条请求,老板来碗臊子面,多加香菜不要辣椒。这个数据就会以二进制的形式出现在requestPacket字节数组中,我们刚刚开辟的空间也就是空饭盒是以byte字节数组出现的,我们需要做的是将这个字节数组转换为String字符串,String里的内容就是老板来碗臊子面,多加香菜不要辣椒。
这个操作并不是强制要求,而是为了后续的代码简单处理。
public void start(){
System.out.println("服务器启动");//提示
while(true){
//每次循环,要做三件事
//1.读取并请求解析
//构造一个空的饭盒方便打饭
DatagramPacket requestPacket = new DatagramPacket(new byte[1080],1080);
socket.receive(requestPacket);
//将字节数组转换为String
String request = new String(requsetPacket.getData(),0,requestPacket.getlength());
}
}
2.根据请求计算响应时间
这里我们只是简单构造一个response作为响应,因为这是一个回显服务器,请求和响应的结果如出一辙。
public void start(){
System.out.println("服务器启动");//提示
while(true){
//每次循环,要做三件事
//1.读取并请求解析
//构造一个空的饭盒方便打饭
DatagramPacket requestPacket = new DatagramPacket(new byte[1080],1080);
socket.receive(requestPacket);
//将字节数组转换为String
String request = new String(requsetPacket.getData(),0,requestPacket.getlength());
//2.根据请求计算响应(此处省略这个步骤)
String response = process(requset);
}
}
public String process(String requsest){
//这个process是根据请求计算响应
//由于是一个回显服务器,写啥就是啥
//之所以单独列出来,就是告知我们,这是服务器中的关键环节
return request;
}
3.把响应结果写回至客户端
客户端的请求发送了,内容也解析了,此时就需要把我们响应的结果写回至客户端。
我们细想一下,从网上买了一本书,这个时候需要发货了,作为卖家我得知道收件人的地址,这样才能把书寄出去,还得把这本书包装起来。我们根据刚才的请求response字符串,构造一个DatagramPacket,和请求不同的是,构造完了需要指定这个包是发送给谁的。
public void start(){
System.out.println("服务器启动");//提示
while(true){
//每次循环,要做三件事
//1.读取并请求解析
//构造一个空的饭盒方便打饭
DatagramPacket requestPacket = new DatagramPacket(new byte[1080],1080);
socket.receive(requestPacket);
//将字节数组转换为String
String request = new String(requsetPacket.getData(),0,requestPacket.getlength());
//2.根据请求计算响应(此处省略这个步骤)
String response = process(requset);
//3.把响应结果写回到客户端
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),
response.getBytes.length,
requestPacket.getSocketAddress());
socket.send(responsePacket);
}
}
public String process(String requsest){
//这个process是根据请求计算响应
//由于是一个回显服务器,写啥就是啥
//之所以单独列出来,就是告知我们,这是服务器中的关键环节
return request;
}
还记得我们是如何构造那个空饭盒的吗,字节数,字节长度。
requestPacket是通过客户端发送过来的,所以getSocketAddress()这个方法就可以得到客户端的IP和端口号。
getSocketAddress同时包含了IP和端口号,此时我们就需要根据这个请求把结果发送回去。
打包裹的过程
发包裹的过程
打印出日志
System.out.printf("[%s:%d] req: %s, resp: %s\n",
requestPacket.getSocketAddress().toString(),
requestPacket.getPort(),request,response);
4.设置端口
public static void main(String[] args) throws IOException {
UdpEchoServer udpEchoServer = new UdpEchoServer(9090);
udpEchoServer.start();
}
写好了服务器,此时我们就需要写客户端的代码
此时也是需要创建一个Socket,发送和接收都需要Socket。
public class UdpEchoClient {
private DatagramSocket socket =null;
private String serverIP;//IP地址
private int serverPort;//端口号
}
除了Socket还需要有地址和端口号,客人来吃饭,总得知道你的店铺在哪里。
public UdpEchoClient(String serverIP, int serverPort) throws SocketException {
//对于客户端来说,不需要显示关联端口
//不代表没有端口,而是系统自动分配了个空闲的端口
socket = new DatagramSocket();
this.serverIP = serverIP;
this.serverPort= serverPort;
}
对于客户端而言是不需要关联端口的,但这不代表客户端没有端口,而是系统自动分配了空闲的端口,客人周一来吃饭坐在C10的位置,周二再来的时候不一定C10就是空出的状态。
启动客户端需要做的操作如下
1.从控制台读取一个字符串
2.把字符串构造成一个UDP Packet,并进行发送
3.客户端尝试读取服务器返回的响应
4.把响应数据转换成 String 显示出来
public void start() throws IOException{
//通过这个客户端可以多次和服务器进行交互
Scanner scanner = new Scanner(System.in);
}
Scanner开始读取了,还是和服务器端一样,启动也是在while循环中。
public void start() throws IOException{
//通过这个客户端可以多次和服务器进行交互
Scanner scanner = new Scanner(System.in);
while(true){
//1.从控制台,读取一个字符串过来
// 先打印一个提示符,提示用户要输入内容
System.out.println("->");
String request = scanner.next();//读取字符串
}
我们在服务端发送了一个请求,这个请求最终被构造成了一个DatagramPacket发送,那么作为客户端的响应,我们也需要将已经读取的字符串构造成一个UDP Packet进行发送。
对比服务器和客户端两段构造UDP的代码看来,不难发现,基本上是一样的,只不过服务器是接收响应,而客户端是构造请求并回复服务器,至于细节都是通过String来读取,这样有利于我们的操作;客户端的构造一方面是需要String中的getBytes数组,另一方面则是需要指定服务器的IP和端口,此处不再是通过InetAddress直接构造了,而是分开构造,服务器在返回响应的时候,是直接从packet里面取出的InetAddress。
public void start() throws IOException{
//通过这个客户端可以多次和服务器进行交互
Scanner scanner = new Scanner(System.in);
while(true){
//1.从控制台,读取一个字符串过来
// 先打印一个提示符,提示用户要输入内容
System.out.println("->");
String request = scanner.next();//读取字符串
//2.把字符串构造成 UDP packet,并进行发送
DatagramPacket requestPacket = new DatagramPacket(
request.getBytes(),
request.getBytes().length,
InetAddress.getByName(serverIP),serverPort);
//取到字节数组,取到数组的长度,把服务器的IP地址构造进去
socket.send(requestPacket);
}
构造完了UDP Packet数据报以后,就需要读取来自服务器的响应了,如何读取呢?还是用到了空饭盒打饭的原理,我们再次构造一个空饭盒用于接收来自服务器的响应。
DatagramPacket responsePacket = new DatagramPacket(new byte[1080],1080);
//构造一个空的字节数组负责去接收
Socket.receive(responsePacket);
//把空的数据传送到receive中,负责填充
OK,那么这个时候已经有来自服务器的响应了,但是这个数据是以二进制的形式展现出来的,所以还是需要以String的方式转换一下
String respponse = new String(responsePacket.getData(),0,responsePacket.getLength());
System.out.printf("req: %s, req: %s\n",request,response);
因为我们是一个回显服务器,是在同一台主机上发送请求和返回响应的,所以我们的端口需要和服务器端保持一致
public static void main(String[] args) throws IOException {
UdpEchoClient udpEchoClient =new UdpEchoClient("127.0.0.1",9090);
udpEchoClient.start();
}
接下来我们将服务器和客户端放在一起看下启动的效果
记住:一定是服务器先启动然后才是客户端启动,面馆是一定先存在于店铺之前
//启动服务器的主逻辑
public void start() throws IOException {
System.out.println("服务器启动!!");
while (true){
//1.读取请求并解析
//构造一个空的饭盒
DatagramPacket requestPacket = new DatagramPacket(new byte[4090],4090);
//食堂大妈给饭盒打饭,饭从网卡里面来
socket.receive(requestPacket);
//为了方便处理请求,把数据报转成String
String request = new String(requestPacket.getData(),0, requestPacket.getLength());
// 2.根据请求计算响应(此处省略这个步骤)
String response = process(request);
// 3.把响应结果写回至客户端
// 根据 response 字符串,构造一个 DatagramPacket
// 和请求 packet不同,此处构造响应的时候,是需要指定这个包是发送给谁的
DatagramPacket responsePacket = new DatagramPacket(
response.getBytes(),
response.getBytes().length,
requestPacket.getSocketAddress());
//requestPacket 是从客户端这里收来的,所以getSocketAddress就会得到客户端的IP和端口
//注意:getSocketAddress同时包含了IP和端口号
socket.send(responsePacket);//发送响应的数据报
System.out.printf("[%s:%d] req: %s, resp: %s\n",
requestPacket.getSocketAddress().toString(),
requestPacket.getPort(),request,response);
}
}
//启动客户端主逻辑
public void start() throws IOException{
//通过这个客户端可以多次和服务器进行交互
Scanner scanner = new Scanner(System.in);
while(true){
//1.从控制台,读取一个字符串过来
// 先打印一个提示符,提示用户要输入内容
System.out.println("->");
String request = scanner.next();//读取字符串
//2.把字符串构造成 UDP packet,并进行发送
DatagramPacket requestPacket = new DatagramPacket(
request.getBytes(),
request.getBytes().length,
InetAddress.getByName(serverIP),serverPort);
//取到字节数组,取到数组的长度,把服务器的IP地址构造进去
socket.send(requestPacket);
//3.客户端尝试读取服务器返回的响应
DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);//构造一个空的字节数组去接受
socket.receive(responsePacket);//把空的数据传到receive中,负责填充
//4.把响应数据转换成 String 显示出来
String response = new String(responsePacket.getData(),0,responsePacket.getLength());
System.out.printf("req: %s, rep: %s\n",request,response);
}
}
1.服务器先启动,直接来到receive这里,并进行阻塞等待
2.客户端开始运行,来客人了,从控制台读取数据,并发送send,此时服务器和客户端都会往下执行代码
3.客户端发送之后,继续往下走,走到receive这里读取响应;
服务器就会从receive这里返回,读到请求数据(由客户端发来的)再往下走到send,并打印日志
4.进入下一轮的循环,服务器再次阻塞在receive,刚才的面已经吃完了,会有新的顾客来点餐;等待下一次请求客户端这边真正收到服务器send回来的数据后就会接触阻塞,有人点单了,执行下面的打印日志操作;如下图,会再次阻塞在receive这里,直到收到有从通过网卡发来的数据。
5.客户端会进入下一轮的循环,阻塞在等待用户输入数据这里->scanner.next。
先启动服务器,提示服务器启动
再启动客户端,输入hello\
服务器返回响应,输出hello\
回显服务器输入是什么,返回的就是什么。