JavaEE 初阶第十九期:网络编程“通关记”(一)
专栏:JavaEE初阶起飞计划
个人主页:手握风云
目录
一、网络编程
1.1. 为什么需要网络编程
1.2. 什么是网络编程
二、Socket套接字
2.1. 分类
2.2. UDP数据报套接字
2.3. 字典服务器
一、网络编程
1.1. 为什么需要网络编程
我们在浏览器中,打开B站或者油管等视频网站,实质是通过网络,获取到网络上的⼀个视频资源。与本地打开视频文件类似,只是视频文件这个资源的来源是⽹络。 相⽐本地资源来说,网络提供了更为丰富的网络资源,如视频、图片、文本、网页。
1.2. 什么是网络编程
网络编程是指编写能够在计算机网络中进行数据传输、通信和交互的程序的过程。它涉及到利用网络协议(如 TCP/IP、HTTP 等),让不同设备(如计算机、服务器、移动设备等)之间能够交换信息,实现资源共享、远程控制、数据同步等功能。
我们写的程序处于应用层,主要工作是调用系统API把数据交给传输层,剩下的都由操作系统、驱动、硬件实现。传输层提供的网络通信API,也称为Socket API(网络编程套接字)。
二、Socket套接字
2.1. 分类
传输层所涉及到的TCP和UDP协议,这两个协议提供了不同的API。流套接字,使用传输层TCP协议,特点:有链接、可靠传输、面向字节流。数据报套接字,使用传输层UDP协议,特点:无连接、不可靠传输、面向数据报。
2.2. UDP数据报套接字
UDP,即User Datagram Protocol。DatagramSocket是UDP Socket API,代表了操作系统的API文件。文件是硬盘设备的“抽象”,读写文件的时候,本质是操作硬盘。网卡硬件设备也是通过文件封装的。通过网络发送数据,需要往网卡中写入;通过网络接受数据,需要从网卡中读取。
Java中创建一个DatagramSocket对象,相当于在操作系统中打开一个Socket文件。这里的Socket文件就代表了网卡。
- DatagramSocket构造方法
方法签名 | 方法说明 |
DatagramSocket() | 创建⼀个UDP数据报套接字的Socket,绑定到本机任意⼀个随机端口(⼀般用于客户端) |
DatagramSocket(int port) | 创建⼀个UDP数据报套接字的Socket,绑定到本机指定的端口(⼀般用于服务端) |
- DatagramSocket方法
方法签名 | 方法说明 |
void receive(DatagramPacket p) | 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待) |
void send(DatagramPacket p) | 从此套接字发送数据报(不会阻塞等待,直接发送) |
void close() | 关闭此数据报套接字 |
- DatagramPacket方法
方法签名 | 方法说明 |
InetAddress getAddress() | 从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址 |
int getPort() | 从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号 |
byte[] getData() | 获取数据包中的数据 |
网络通信的基本流程:客户端(client),主动发起通信;服务器(server),被动接受通信。客户端向服务器发起一个请求,等待客户端一个响应,这就是最简单的“一问一答”模型。
一个最简单的网络程序,客户端读取用户输入的内容,通过网络发送给服务器,服务器从客户端读取到请求内容,然后根据请求计算响应,接着把响应返回到客户端,客户端读到相应之后,最后把响应结果显示到控制台上。
import java.net.DatagramSocket;
import java.net.SocketException;public class EchoServer {// 创建Socket对象private DatagramSocket socket;public EchoServer(int port) throws SocketException {socket = new DatagramSocket(port);}
}
接下来的网络通信都是依赖于上面定义的socket对象。当这个对象创建好之后,此时这个socket与就关联到了一个端口号port,也称为“绑定端口号”,通过端口号来区分程序。服务器和客户端启动的时候,都会绑定一个端口号,假设服务器端口号为9090,客户端端口号为1234。当客户端给发送请求时,源端口就是1234,目的端口是9090;当服务器返回给客户端响应时,源端口是9090,目的端口就是1234。端口号在网路协议中,是使用两个字节表示的无符号整数。
我们需要指定的是空闲端口,一个端口同一时刻,只能被一个端口绑定。如果在绑定的时候改端口被其他进程占用,就会绑定失败并抛出异常。要想知道哪些端口号被占用,我们既可以尝试绑定,也可以使用一些命令来查看。比如Windows中的cmd窗口中输入"netstat"命令。
// 启动服务器,完成业务逻辑
public void start() throws IOException {while (true) {// 1、读取请求并解析DatagramPacket reqPacket = new DatagramPacket(new byte[4096], 4096);socket.receive(reqPacket);// 处理数据// 发送数据}
}
这里创建了一个DatagramPacket实例,它是UDP通信中用于接收或发送数据包的容器。当数据包到达receive时,它的内容会被存储在reqPacket的缓冲区中;同时,数据包的源地址和端口信息也会被存储在reqPacket对象中。
如果服务器一启动,就会执行到receive,如果没有客户端发送任何请求,receive就会阻塞。
DatagramPacket resPacket = new DatagramPacket(response.getBytes(), response.getBytes().length);
socket.send(resPacket);
将字符串response转换为字节数组。因为在网络传输中,数据是以字节形式传输的。还要获取获字节数组的长度,表示数据包的大小。send()方法将数据包通过网络发送出去。
当进行到send()方法时,要把resPacket返回给谁?因为一个服务器对应多个客户端,原则上,谁发送的请求就得返回给谁。UDP socket里面是不保存对方的信息的。服务器收到客户端的请求,不光是一个UDP数据包,UDO外面有IP,IP外面有以太网数据帧,这是receive收到的完整数据报,虽然以太网报头和IP报头都会被操作系统解析出来,但仍然可以通过DatagramPacket获取到完整信息。
// 在构造方法里面加上一个参数
DatagramPacket resPacket = new DatagramPacket(response.getBytes(), response.getBytes().length, reqPacket.getSocketAddress());
完整代码实现:
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;public class EchoServer {// 创建Socket对象private DatagramSocket socket;public EchoServer(int port) throws SocketException {socket = new DatagramSocket(port);}// 启动服务器,完成业务逻辑public void start() throws IOException {System.out.println("服务器启动");while (true) {// 1、读取请求并解析DatagramPacket reqPacket = new DatagramPacket(new byte[4096], 4096);socket.receive(reqPacket);// 把DatagramPacket里面的数据解析成字符串String request = new String(reqPacket.getData(), 0, reqPacket.getLength());// 根据请求计算响应String response = process(request);// 把响应写回到客户端DatagramPacket resPacket = new DatagramPacket(response.getBytes(), response.getBytes().length, reqPacket.getSocketAddress());socket.send(resPacket);System.out.printf("[%s:%d] 请求: %s 响应: %s\n", reqPacket.getAddress(), reqPacket.getPort(), request, response);}}private String process(String request) {return request;}public static void main(String[] args) throws IOException {EchoServer echoServer = new EchoServer(9090);echoServer.start();}
}
以上就是服务器的编写,下面要完成客户端的编写:
import java.net.DatagramSocket;
import java.net.SocketException;public class EchoClient {private DatagramSocket socket;private String severIP;private int serverPort;public EchoClient(String serverIP, int serverPort) throws SocketException {socket = new DatagramSocket();this.severIP = serverIP;this.serverPort = serverPort;}
}
客户端在创建socket对象的时候,不需要指定端口号,操作系统会自动分配一个端口。对于服务器来说,服务器在程序员手里,如果出现冲突会很容易处理;对于客户端来说,是在用户自己的电脑上,很可能指定的端口会与用户电脑上的其他进程产生冲突。
完整代码实现:
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.Scanner;public class EchoClient {private DatagramSocket socket;private String severIP;private int serverPort;public EchoClient(String serverIP, int serverPort) throws SocketException {socket = new DatagramSocket();this.severIP = serverIP;this.serverPort = serverPort;}public void start() throws IOException {Scanner in = new Scanner(System.in);System.out.println("客户端启动:");while (true) {// 1、从控制台输入用户读取的内容System.out.print(">");String request = in.nextLine();// 2、构造成UDP请求,并发送DatagramPacket reqPacket = new DatagramPacket(request.getBytes(), request.getBytes().length, InetAddress.getByName(severIP), serverPort);socket.send(reqPacket);// 3、读取服务器的响应DatagramPacket resPacket = new DatagramPacket(new byte[4096], 4096);socket.receive(resPacket);String response = new String(resPacket.getData(), 0, resPacket.getLength());// 4、把响应显示到控制台System.out.println(response);}}public static void main(String[] args) throws IOException {EchoClient client = new EchoClient("127.0.0.1", 9090);client.start();}
}
"127.0.0.1"是一个环回IP,当服务器和客户端在同一个主机上时,无论主机真实的IP是啥,都可以通过127.0.0.1访问。
先运行服务器在运行客户端,运行结果如下:
但这只是自己的主机上进行通信,我们还得进行跨主机通信。如果是上面这个服务器,只有在同一局域网连接下,才能实现别的主机连接。也可以把这个程序放在云服务器上,就可以不受同一局域网的限制了。
上面的逻辑,通过多主机完成,本质上引入了更多的“硬件资源”,假如某个程序计算响应的过程非常复杂,非常消耗资源,那我们就可以交给算力更强的服务器来解决。
2.3. 字典服务器
编写一个英译汉的服务器,我们新创建一个DictServer类,直接继承EchoServer,里面的逻辑我们都可以直接拿过来用,只不过返回的请求需要修改,也就是需要重写process()方法。
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;public class DictServer extends EchoServer {private HashMap<String, String> dict = new HashMap<>();public DictServer(int port) throws SocketException {super(port);dict.put("apple", "苹果");dict.put("banana", "香蕉");dict.put("dog", "小狗");dict.put("cat", "小猫");dict.put("browser", "浏览器");}@Overridepublic String process(String request) {return dict.getOrDefault(request, "该单词没有查到");}public static void main(String[] args) throws IOException {DictServer dictServer = new DictServer(9090);dictServer.start();}
}
但是一运行,抛出了如下异常,因为启动了两个服务器,导致端口号被占用,只需要退出其中一个就可以。