🎇个人主页:Ice_Sugar_7
🎇所属专栏:计网
🎇欢迎点赞收藏加关注哦!
实现回显服务器
🍉socket api
操作系统给我们提供的进行网络编程的 api 称为 socket api
(网络编程套接字),具体到传输层,有两个重要的协议的 api —— UDP api
和 TCP api
,本文我们介绍的是 UDP api
UDP 有四个特点:无连接、不可靠传输、面向数据报、全双工。这在后文中会解释
Java 对系统原生的 api 进行了封装,UDP socket 有两个核心的类
- DatagramSocket
操作系统中有一类文件,叫作 socket 文件
,它和我们之前所说的“文件”不太一样,我们平时所说的普通文件、目录文件位于硬盘
上,而 socket 文件则是抽象表示了网卡这样的硬件设备(网卡是网络通信中的核心硬件设备),也就是把网卡等硬件视为一种文件。通过网卡发送数据就是写 socket 文件;接收数据就是读 socket 文件
说回 DatagramSocket,它负责读写 socket 文件
,也就是借助网卡发送或接收数据
它有两个构造方法:
构造方法 | 说明 |
---|---|
DatagramSocket() | 创建一个 UDP 数据报套接字的 Socket,绑定到本机任意一个随机端口(一般用于客户端) |
DatagramSocket(int port) | 创建一个 UDP 数据报套接字的 Socket,绑定到本机指定的端口,即 port(一般用于服务器) |
负责发送和接收的方法如下:
方法 | 说明 |
---|---|
void reveive(DatagramPacket p) | 让 p 接收数据报(如果没接收到数据报,这个方法就会阻塞等待 )(注意这里的参数是输出型参数,实际上 DatagramPacket 内部包含了一个字节数组 ) |
void send(DatagramPacket p) | 从 p 发送数据报(直接发送出去,不会阻塞) |
- DatagramPacket
DatagramPacket 表示一个 UDP 数据报。UDP 面向数据报,每次发送、接收数据的基本单位就是一个 UDP 数据报
🍉回显服务器
这是网络编程中最简单的程序,相当于 hello world,不过还是有一定的难度
服务器在接收客户端的请求后会返回响应,具体返回什么响应,要根据实际的业务场景分析。对于回显服务器,它没有业务逻辑,客户端发什么请求,服务器就返回什么响应
🍌实现
接下来我们通过 UDP 协议来实现一个回显服务器
🥝服务器
首先要创建一个 DatagramSocket 对象,然后要通过这个 socket 对象来操作网卡
public class UdpEchoServer { DatagramSocket socket = null; public UdpEchoServer(int port) throws SocketException { socket = new DatagramSocket(port); //在运行一个服务器程序时,通常会手动指定端口 } }
补充:这里的 SocketException 是网络编程中一个常见的异常,通常表示 socket 创建失败,比如端口号已经被别的进程占用了
接下来服务器主要做三件事
①读取请求并解析
②根据请求计算响应。对于回显服务器来说,这一步啥都不用做
③把响应返回到客户端
要读取请求得先创建一个 DatagramPacket 接收请求
DatagramPacket requestPacket = new DatagramPacket(new byte[1024],1024); socket.receive(requestPacket); String request = new String(requestPacket.getData(),0,requestPacket.getLength());
使用字节数组构造字符串的方法一定要记住
调用 receive 涉及到缓冲区,下面通过图示补充一下:
第二步是根据请求计算响应,虽然回显服务器这一步不用做什么,不过为了逻辑完整,我们写一个 process 方法,它只返回 request
(如果是具有特定业务的服务器,process 中就写其他你想要的逻辑)
public String process(String request) { return request; }
最后就是把响应返回给客户端,这一步要用到 send
方法
//3.把响应返回到客户端 //构造一个 DatagramPacket 作为响应对象 DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),0, response.getBytes().length,requestPacket.getSocketAddress()); socket.send(responsePacket);
接下来在主方法中启动服务器
服务器的代码如下:
public class UdpEchoServer { DatagramSocket socket = null; public UdpEchoServer(int port) throws SocketException { socket = new DatagramSocket(port); } //服务器的启动逻辑 public void start() throws IOException { System.out.println("服务器启动"); while(true) { //每次循环就是处理一个请求,然后返回响应的过程 DatagramPacket requestPacket = new DatagramPacket(new byte[1024],1024); //1.读取请求并解析 socket.receive(requestPacket); //填充字节数组后,将其转为 String 方便后续处理逻辑 //getData 方法获取到 DatagramPacket 内部的字节数组 String request = new String(requestPacket.getData(),0,requestPacket.getLength()); //2.根据请求计算响应 String response = process(request); //3.把响应返回到客户端 //构造一个 DatagramPacket 作为响应对象 DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),0, response.getBytes().length,requestPacket.getSocketAddress()); socket.send(responsePacket); //打印日志 System.out.printf("[%s:%d] req:%s, resp:%s\n",requestPacket.getAddress().toString(), requestPacket.getPort(),request,response); } } public String process(String request) { return request; } public static void main(String[] args) throws IOException { UdpEchoServer server = new UdpEchoServer(7000); server.start(); } }
🥝客户端
接下来编写客户端的代码
首先要创建 socket 对象,注意客户端这里不需要手动指定端口号
1.在代码中手动指定端口号,可以保证端口号始终固定;如果不手动指定,那就是系统自动分配,这样的话服务器每次重启之后端口号可能就变了,一旦变了,客户端就可能找不到服务器在哪儿了,所以服务器需要手动指定
2.而对于客户端,因为无法确保手动指定的端口是可用的(可能被其他进程占用了),这就可能导致程序因为端口绑定失败而无法启动,所以让系统随机分配一个空闲的端口就 ok 了
接下来客户端要做四件事
1.从控制台读取要发送的请求数据
2.构造请求并发送
3.读取服务器的响应
4.把响应显示到控制台上
第一步就是先创建一个 Scanner
对象来读取字符串
这里补充一点,使用 Scanner 从控制台读取字符串的话最好使用 next,因为如果用 nextLine 读取需要手动输入换行符 enter,但是 enter 键除了产生 \n 还会产生其他字符,这就会导致读取到的内容容易出问题;而如果从文件读取的话那就用哪个都行
第二步构造请求就用接收的字符串来构造一个 DatagramPacket
对象,然后发送
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName(serverIp),serverPort); socket.send(requestPacket);
到这里我们已经了解了三种构造 DatagramPacket 对象的方法,总结一下:
//第一种:搭配 receive 使用。构造的时候指定空白的字节数组 DatagramPacket requestPacket = new DatagramPacket(new byte[1024],1024); //第二种:发数据时使用。构造时指定有内容的字节数组,并指定 IP 和端口 DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length, requestPacket.getSocketAddress()); //第三种:发数据时使用。构造时指定有内容的字节数组,并指定 IP 和 端口,这两者分开指定 DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName(serverIp),serverPort);
回到正题,第三步是读取服务器响应
DatagramPacket responsePacket = new DatagramPacket(new byte[1024],1024); socket.receive(responsePacket);
最后就是把它显示到控制台:
String response = new String(responsePacket.getData(),0,responsePacket.getLength()); //再次强调,这里的 getLength 方法得到的是有效长度 System.out.println(response);
客户端代码如下:
public class UdpEchoClient { DatagramSocket socket; String serverIp; int serverPort; public UdpEchoClient(String serverIp,int serverPort) throws SocketException { this.serverIp = serverIp; this.serverPort = serverPort; socket = new DatagramSocket(); } public void start() throws IOException { System.out.println("客户端启动"); Scanner scanner = new Scanner(System.in); while(true) { if(!scanner.hasNext()) break; //1. 从控制台读取要发送的请求数据 String request = scanner.next(); //2.构造请求并发送 DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName(serverIp),serverPort); socket.send(requestPacket); //3.读取服务器的响应 DatagramPacket responsePacket = new DatagramPacket(new byte[1024],1024); socket.receive(responsePacket); //4.把响应显示到控制台上 String response = new String(responsePacket.getData(),0,responsePacket.getLength()); System.out.println(response); } } public static void main(String[] args) throws IOException { UdpEchoClient client = new UdpEchoClient("127.0.0.1",7000); client.start(); } }
接下来运行一下看看效果: