目录
3.3 基于 UDP socket 写一个简单的回显客户端服务器程序(一发一收)
3.4 写一个简单的单词翻译服务器(请求是一个英文单词,响应是这个单词的中文翻译)
1.网络编程基础
1.1 为什么需要网络编程?
用户在浏览器中,打开在线视频网站,如优酷看视频,实质是通过网络,获取到网络上的一个视频资源。
与本地打开视频文件类似,只是视频文件这个资源的来源是网络。相比本地资源来说,网络提供了更为丰富的网络资源:所谓的网络资源,其实就是在网络中可以获取的各种数据资源。
1.2 什么是网络编程
网络编程,指网络上的主机,通过不同的进程,以编程的方式实现网络通信(或称为网络数据传输)。
1.3 网络编程中的基本概念
1️⃣发送端和接收端
发送端:数据的发送方进程,称为发送端。发送端主机即网络通信中的源主机
接收端:数据的接收方进程,称为接收端。接收端主机即网络通信中的目的主机。
2️⃣请求和响应
一般来说,获取一个网络资源,涉及到两次网络数据传输:
第一次:请求数据的发送第二次:响应数据的发送
3️⃣客户端和服务端
服务端:在常见的网络数据传输场景下,把提供服务的一方进程,称为服务端,可以提供对外服务。
客户端:获取服务的一方进程,称为客户端。
2.Socket套接字
Socket套接字,是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。
2.1 分类
1️⃣流套接字:使用传输层TCP协议
有连接:使用 TCP 通信的双方,则需要刻意保存对方的相关信息可靠传输面向字节流:以字节为传输的基本单位,读写方式非常灵活全双工:一条路径,双向通信
2️⃣数据报套接字:使用传输层UDP协议
无连接:使用 UDP 通信的双方,不需要可以保存对端的相关信息
不可靠传输
面向数据报:以一个 UDP 数据报为基本单位
全双工:一条路径,双向通信
- 全双工:双向通信,一个管道,能 A->B,B->A 同时进行
- 半双工:单向通信,一个管道,同一时刻,要么 A->B,要么 B->A ,不能同时进行
3.UDP数据报套接字编程
3.1 DatagramSocket API
Datagram——数据报
Socket——说明这个对象是一个 socket 对象(相当于对应到系统中一个特殊的文件(socket文件),socket 文件并非对应硬盘上的某个数据存储区域)
DatagramSocket 是 UDP Socket,用于发送和接收UDP数据报
DatagramSocket 构造方法: | ||
DatagramSocket() | DatagramSocket() 创建一个 UDP 数据报套接字的 Socket,绑定到本机任意一个随机端口(一般用于客户端) | |
DatagramSocket(int port) | DatagramSocket(int port) 创建一个 UDP 数据报套接字的 Socket,绑定到本机指定的端口(一般用于服务端) |
DatagramSocket 方法: | ||
void receive(DatagramPacket p) | void receive(DatagramPacket p) 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待) | |
void send(DatagramPacket p) | void send(DatagramPacket p) 从此套接字发送数据报包(不会阻塞等待,直接发送) | |
void close() | void close() 关闭此数据报套接字 |
3.2 DatagramPacket API
DatagramPacket是UDP Socket发送和接收的数据报。
DatagramPacket 构造方法: | ||
DatagramPacket(byte[] buf, int length) | DatagramPacket(byte[] buf, int length) 构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中,接收指定长度(第二个参数length) 这个版本不需要“设置地址进去”,通常用来“接收消息” | |
DatagramPacket(byte[] buf, int offset, int length,SocketAddress address) | DatagramPacket(byte[] buf, int offset, int length,SocketAddress address) 构造一个DatagramPacket以用来发送数据报,发送的数据为字节数组(第一个参数buf)中,从0到指定长度(第二个参数length)。address指定目的主机的IP和端口号 这个版本需要“显式的设置地址进去”。通常用来“发送消息” |
DatagramPacket 方法: | ||
InetAddress getAddress() | InetAddress getAddress() 从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址 | |
int getPort() | int getPort() 从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号 | |
byte[] getData() | byte[] getData() 获取数据报中的数据 |
🌈构造UDP发送的数据报时,需要传入 SocketAddress ,该对象可以使用 InetSocketAddress 来创建。
3.3 基于 UDP socket 写一个简单的回显客户端服务器程序(一发一收)
回显服务器(echo server) 客户端发了一个请求,服务端返回一个一模一样的响应;一个服务器主要做三个核心工作:
1️⃣读取请求并解析
2️⃣根据请求计算响应(省略了)
3️⃣把响应返回到客户端
3.3.1 UDP 服务端
socket = new DatagramSocket(port);
1️⃣绑定一个端口 => 把这个进程和一个端口号关联起来
一个操作系统上面,有很多端口号,0 - 65535 。
程序如果需要进行网络通信,就需要获取到一个端口号,端口号相当于用来在网络上区分进程的身份标识符。(操作系统收到网卡数据,就可以根据网络数据报中的端口号,来确定要把这个数据交给哪个进程)
2️⃣分配端口号的过程:
手动指定:
new DatagramSocket(port);
系统自动分配:
socket = new DatagramSocket();
一个端口,在通常情况下,是不能被同一个主机上的多个进程同时绑定的;一个进程是可以绑定多个端口的。
如果端口已经被别人占用,再尝试绑定,就会抛出异常 throws SocketException
- 读取客户端发来的请求,尝试读取,不是说调用了就一定能读到
//1.读取客户端发来的请求
socket.receive();
如果客户端没有发来请求,receive 就会阻塞等待,直到真的有客户端的请求过来了,receive 才会返回。
- 对请求进行解析,把 DatagramPacket 转成一个 String
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
- 根据请求,处理响应,虽然这里此处是个回显服务器,但是还是可以单独搞个方法来做这个事情
String response = process(request);
通过这个方法,实现根据请求计算响应,这个过程由于是回显服务器,所以涉及不到其他逻辑,但是如果是其他服务器,就可以在 process 里面,加上一些其他逻辑的处理
public String process(String req){
return req;
}
- 把响应构造成 DatagramPacket 对象(构造响应对象,要搞清楚,对象要发给谁,谁给咱发的请求,就把响应发给谁)
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());
这个也是构造 DatagramPacket 的一种方式,先是拿字符串里面的字节数组,来构造 Packet 的内容,还要把请求中的客户端地址拿过来,也填到包裹里去。
response.getBytes().length 可以写作 response.length 吗?
不行 response.getBytes().length 表示的是字节数,response.length 表示的是字符数
requestPacket.getSocketAddress() -> (地址)IP + 端口
- 把这个 DatagramPacket 对象返回给客户端
socket.send(responsePacket);
System.out.printf("[%s:%d] req = %s; resp = %s\n",requestPacket.getAddress().toString(),requestPacket.getPort(),request,response);
UDP 服务端总代码:
package Network;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;
/**
* Created with IntelliJ IDEA.
* Description:
* User: Lenovo
* Date: 2023-04-02
* Time: 16:05
*/
//UDP 服务端
public class UdpEchoServe {
//需要先定义一个 socket 对象:通过网络通信,必须要使用 socket 对象
private DatagramSocket socket = null;
//绑定一个端口,不一定能成功
//如果某个端口已经被别的进程占用了,此时这里的绑定操作就会出错
//同一个主机上,一个端口同一时刻,只能被一个进程绑定
public UdpEchoServe(int port) throws SocketException {
//构造 socket 的同时,指定要关联/绑定的端口
socket = new DatagramSocket(port);
}
//启动服务器的主逻辑
public void start() throws IOException {
System.out.println("服务器启动!");
//一旦服务器已启动,调用 start 方法,就会立即执行到 receive;
//此时,还没有客户端达赖数据,此时 receive 就会阻塞等待,直到传输过来数据
while (true) {
//每次循环,要做三件事:
//1.读取请求并解析(构造空饭盒)
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);//空的对象,空的对象持有了一个空的字节数据
//在Java中,[] 和 . 是解引用;直接 = 不是,内部要想影响到外部,就需要通过解引用进行的
//receive 参数类型是 DatagramPacket,是引用参数,即receive 方法内部,针对参数进行修改,外部也生效
socket.receive(requestPacket);
//为了方便处理这个请求,把数据报转换成 String
//这个操作不是必须的,只是此处为了后续代码简单,就简单构造一个String,拿着requestPacket 中持有的字节数组进行构造
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());
socket.send(responsePacket);
System.out.printf("[%s:%d] req: %s, resp: %s\n", requestPacket.getAddress().toString(),
requestPacket.getPort(), request, response);
}
}
//这个方法希望是根据请求计算响应
//由于写的是一个 回显 程序,请求是啥,响应是啥
//如果后续写个别的服务器,不再回显了,而是有具体的业务了,就可以修改 process 方法,根据需要来重新构造响应
//之所以单独列成一个方法,这是一个服务器中的关键环节
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServe udpEchoServe = new UdpEchoServe(9090);
udpEchoServe.start();
}
}
3.3.2 UDP客户端
服务器,端口一般是手动指定的,如果自动分配,客户端就不知道服务器的端口是啥了,因此服务器有固定端口客户端才好访问。
客户端,端口一般是自动分配的,客户端程序是安装在用户的电脑上的,用户电脑当前运行哪些程序,是不可控的,如果要是手动指定端口,说不好这个端口就和其他程序冲突了,导致咱们的代码无法运行。
public UDPEchoClient() throws SocketException {
//客户端的端口号,一般都是由操作系统自动分配的,虽然手动指定也行,习惯上还是自动分配比较好
socket = new DatagramSocket();
}
- 让客户端从控制台获取一个请求数据
System.out.println("> ");
String request = scanner.next();
- 把这个字符串请求发送给服务器,构造 DatagramSocket,构造的 Packet 既要包含 要传输的数据,又要包含把数据发送到哪里(另外一种 DatagramPacket 的构造方法)
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName("127.0.0.1"),8000);
InetAddress.getByName(“127.0.0.1”) : 通过这个字符串来构造的 InetAddress,此处的 127.0.0.1 回环 IP 就表示当前主机。
8000 : 服务器端口号
这个包裹,就是要从客户端发送给服务器,就需要知道,发送的内容,以及发送的目的地是哪里(收件人地址 + 端口)
- 把数据报发送给服务器
socket.send(requestPacket);
- 从服务期读取响应数据
DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
socket.receive(responsePacket);
- 把响应数据获取出来,转成字符串
String response = new String(responsePacket.getData(),0,responsePacket.getLength());
System.out.printf("req: %s;resp: %s\n",request,response);
UDP 客户端总代码:
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIP;
private int serverPort;
// 客户端启动, 需要知道服务器在哪里!!
public UdpEchoClient(String serverIP, int serverPort) throws SocketException {
// 对于客户端来说, 不需要显示关联端口.
// 不代表没有端口, 而是系统自动分配了个空闲的端口.
socket = new DatagramSocket();
this.serverIP = serverIP;
this.serverPort = serverPort;
}
public void start() throws IOException {
// 通过这个客户端可以多次和服务器进行交互.
Scanner scanner = new Scanner(System.in);
while (true) {
// 1. 先从控制台, 读取一个字符串过来
// 先打印一个提示符, 提示用户要输入内容
System.out.print("-> ");
String request = scanner.next();
// 2. 把字符串构造成 UDP packet, 并进行发送.
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
InetAddress.getByName(serverIP), serverPort);
socket.send(requestPacket);
// 3. 客户端尝试读取服务器返回的响应
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);
// 4. 把响应数据转换成 String 显示出来.
String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
System.out.printf("req: %s, resp: %s\n", request, response);
}
}
public static void main(String[] args) throws IOException {
// UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1", 9090);
UdpEchoClient udpEchoClient = new UdpEchoClient("42.192.83.143", 9090);
udpEchoClient.start();
}
}
3.3.3 服务器和客户端它们的交互过程
1️⃣服务器先启动,执行到 receive 进行阻塞
2️⃣由于阻塞,服务器接下来的代码就不执行了,此时执行客户端:客户端通过 Scanner 读取请求执行 send 操作
3️⃣此时,客户端和服务器都会往下执行代码:客户端执行 receive 读取响应,会阻塞等待;服务器从 receive 返回,读到请求数据(客户端发来的),执行到 process 生成响应,执行到 send 发送请求,并且打印日志
4️⃣服务器进入下一轮循环,再次阻塞在 receive,等待客户端下一次请求;客户端真正收到服务器 send 回来的数据后就会解除阻塞,执行下边的打印日志
4️⃣客户端继续进行下一轮循环,阻塞在 Scanner. next 这里等待用户输入新的数据
3.3.4 整体效果演示
- 先启动服务器,再启动客户端
客户端中输入一个hello:
在服务器中:
继续在客户端中输入一个你好:
服务器中显示:
3.3.5 一个服务器是可以同时给多个客户端提供服务的
在 idea 中启动多个客户端,需要配置,默认一个程序只能启动一个
如在 IDEA 中,你想打开多个客户端,你发现你再运行一次客户端,就会把之前的客户端给关闭了,此时我们需要设置一下,就可以启动多个客户端。
此时我们就可以打开多个客户端了
3.4 写一个简单的单词翻译服务器(请求是一个英文单词,响应是这个单词的中文翻译)
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
//字典服务器 / 翻译服务器
//希望实现一个英译汉的效果
//请求的是一个英文单词,响应是对应的中文翻译
public class UDPDicServer extends UDPEchoServer{
private Map<String, String> dic = new HashMap<>();
public UDPDicServer(int port) throws SocketException {
super(port);
//这里的数据可以无限的构造下去
//即使是有道词典这种,也是类似的方法实现(打表)
dic.put("cat","小猫");
dic.put("dog","小狗");
dic.put("fuck","卧槽");
}
//和 UDPEchoServer 相比,只是 process 不同,就重写这个方法即可
public String process(String req){
return dic.getOrDefault(req,"这个词俺也不会!");
}
public static void main(String[] args) throws IOException {
UDPDicServer server = new UDPDicServer(8000);
server.start();
}
}