目录
1. 网络编程
在网络通信中, 数据的发送是从应用层开始, 一直封装到物理层然后进行发送的, 应用层要将数据交给传输层进行封装; 而接收方拿到数据后是从物理层到应用层进行分用, 传输层要将拿到的数据再分用给应用层进行使用, 网络编程实际操作中最关键的就是我们所能控制的应用层和传输层之间的交互, 而在操作系统中提供了一组API即socket, 用来实现应用层和传输层之间的交互, Java当中把操作系统提供的API进行了进一步封装以便我们进行使用.
系统提供的Socket API 主要是两组:基于TCP协议的API 和 基于UDP协议的API
TCP和UDP这两者的协议之间差别很大,提供的api也差异很大.下面简要概括主要的区别.
TCP UDP 有连接 无连接 可靠传输
不可靠传输 面向字节流 面向数据报 全双工 全双工 1. 什么是有连接?
使用TCP协议, 必须是通信双方先建立连接才能进行通信(想象打电话的场景这就是有连接), 而使用UDP协议在无连接的情况下可以进行通信(想象发微信, 短信的场景).连接并不是拿一根绳子,把两个设备绑定到一块,而是一个抽象的连接,可以理解成彼此双方记录了对方的信息.比如结婚领证,两个人在民政局进行登记结婚,然后民政局就会给双方发一个结婚证,结婚证是一式两份,供双方所有,这个就是彼此双方做了一个记录,双方进行了连接.
2. 什么是不可靠性传输?
可靠与不可靠传输指的不是安全性质, 而是说你发送出数据后, 能不能判断对方已经收到, 如果能够确定对方是否收到则就是可靠传输, 否则就是不可靠传输.UDP进行传输,只会关注发送消息,而不会关注发送的消息有没有进行接收.TCP不仅关注发送消息本身而且关注消息是否发送到达.(但是不是说通过TCP就会百分百将消息送达,也会出现丢包的行为.)
3. 什么是全双工?
全双工是指一条通信链路, 可以双向传输(同一时间既可以发, 也可以收); 而半双工是一条链路, 只能单向通信.
4. 面向字节流和面向数据报?
面向字节流就类文件读写数据的操作, 是 “流” 式的; 而面向数据报的话数据传输则是以一个个的 “数据报” 为基本单位(一个数据报可能是若干个字节, 是带有一定的格式的).
2. UDP网络编程
2.1 DatagramSocket API
这里receive
方法参数传入的是一个空的对象, receive方法内部会对这个对象进行填充, 从而构造出结果数据, 这个参数也是一个输出型参数.
2.2 DatagramPacket API
第二个是DatagramPacket
, 表示一个UDP数据报, 在UDP的服务器和客户端都需要使用到, 接收和发送的数据就是在传输DatagramPacket对象, 这就是体现了UDP面向数据报的特点.
在网络编程中, 一定要注意区分清楚服务器与客户端使用之间使用的五元组, 具体如下:
- 源IP, 就是发送方IP.
- 源端口, 发送方端口号, 服务器需要手动指定, 客户端让系统随机分配即可.
- 目的IP, 接收方IP, 包含在拿到的数据报中, 服务器响应时的目的IP就在客户端发来的数据报中, 客户端发送请求时的目的IP就是服务器的IP.
- 目的端口, 接收方端口号包含在数据报中, 服务器响应时的目的端口就在客户端发来的数据报中, 客户端发送请求时的目的端口.就是服务器的端口号
- 协议类型, 如UDP/TCP.
2.3 基于UDP实现的回显服务器
正常来说服务器为客户端进行提供服务,客户端向服务器发出请求,服务端根据请求来计算响应,然后将响应发送给客户端,这个过程是正常的一个网络通信,在这里本文省略计算响应的环节,服务器将客户端发送的请求进行返回.这就是一个回显程序.
2.3.1 UDP服务器设计步骤
- 1. 创建Socket实例对象(DatagramSocket对象),需要指定服务器的端口,因为服务器是被动请求的一段,所以得先知道端口号,才能发出请求.
- 2. 服务器启动,读取客户端的请求,将数据填充到DatagramPacket对象中,这里的请求是包含着有客户端的地址信息(IP+端口号,通过getSocketAddress获取).
- 3. 处理客户端请求,计算响应,这里实现的是一个回显程序,直接根据请求返回来客户端的请求即可.
- 4. 将响应返回给客户端,需要将响应打包成DatagramPacket对象,此处打包的过程跟请求的packet是不一样的,需要指定这个包发送给谁,使用getSocketAddress()进行获取.
2.3.2 服务器代码
package UDP;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
/**
* Created with IntelliJ IDEA.
* Description:udp 服务端
* 过程:1.客户端进行请求(requestPacket),服务端进行接收请求(receive)
* 2.将请求转换成字符串
* 3.根据请求进行计算响应(需要返回给客户端的数据)
* 4.将相应装包成responsePacket(对比requestPacket参数多了一个requestPacket的ip和端口号(responsePacket.getSocketAddress()))
* 5.将打包好的responsePacket,使用send进行发送(返回给客户端)
* 1. DatagramSocket是文件(快递小哥)
* 2. DatagramPacket是需要传输的数据包(快递包裹)
* User: YAO
* Date: 2023-05-28
* Time: 20:12
*/
public class udpServer {
//1. 先定一个socket对象
// 通过网络通信必须要有socket对象
private DatagramSocket socket = null;
//2. 创建构造方法
// 绑定一个端口号不一定会成功,有可能被别的应用程序占用
// 同一个主机,同一个端口,同一时刻,只能被一个进程绑定
public udpServer(int port) throws SocketException {
// 构造socket的同时,指定关联/绑定的端口
socket = new DatagramSocket(port);
}
//3. 启动服务器
public void start() throws IOException {
System.out.println("服务器启动");
while (true){
// 每次循环,要做三件事情:
//1.读取请求并解析
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
socket.receive(requestPacket); // 此时的参数为输出型参数,传入一个空的对象,交给receive内部进行填充
//1.1 当服务器启动,start方法被调用就会直接执行到receive这里,如果客户端还没进行发送消息,服务器就会进如堵塞等待
//1.2 为了方便处理这个请求,我们将这个请求转换成String类型
// System.out.println(Arrays.toString(requestPacket.getData()));
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
//在这里,使用了String类的构造函数来将字节流转换为字符串。
// 该构造函数有三个参数:
// 第一个参数是要转换成字符串的字节数组
// 第二个参数表示从字节数组中哪个位置开始转换
// 第三个参数表示要转换多少个字节。
//2. 根据请求计算响应(此处省略)
String response = process(request);
//3. 将结果写会给客户端
// 3.1 根据response字符串,构造一个返回给客户端的DatagramPacket
// 3.2 和请求packet是不一样的,此处构造相应的时候,需要指定这个包要发送给谁(客户端ip和端口号)
// :使用requestPacket.getSocketAddress() 这个方法包含了ip和端口号
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(StandardCharsets.UTF_8),
response.getBytes().length,requestPacket.getSocketAddress());
// 3.3 打包完成,进行发送
socket.send(responsePacket);
System.out.printf("[%s:%d] req: %s, resp: %s\n", requestPacket.getAddress().toString(),
requestPacket.getPort(),request, response);
}
}
//4. 根据请求进行计算响应
//这是一个回显程序,就是客户端发送什么,服务器会送什么,后续可以根据自己需要指定返回的内容,这个函数在服务器中为一个关键
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
udpServer udpServer = new udpServer(9090);
udpServer.start();
// 此处socket结束整个main方法结束,整个进程就结束了,所以就不需要进行使用socket.close();
}
}
2.3.3 客户端代码
package UDP;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
/**
* Created with IntelliJ IDEA.
* Description:udp 客户端
* User: YAO
* Date: 2023-05-28
* Time: 20:13
*/
public class udpClient {
private DatagramSocket socket = null;
private final String serverIP;
private final int serverPort;
// 客户端,需要知道服务器在哪?(IP和Port)
public udpClient(String serverIP, int serverPort) throws SocketException {
// 对于客户端,不需要显示关联的的端口
// 不代表就是没有端口,而是系统自动分配了一个端口
socket = new DatagramSocket();
this.serverPort = serverPort;
this.serverIP = serverIP;
}
public void start() throws IOException {
while (true){
// 1. 先从控制台读取一个字符串
Scanner scanner = new Scanner(System.in);
System.out.println("请输入:");
String request = scanner.next();
// 2. 将字符串构造成UDP packet ,并进行发送
// 2.1 转换成二进制的字符数组,指定的长度,指定服务器的IP,服务器的端口
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 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 {
udpClient udpClient = new udpClient("127.0.0.1",9090);
udpClient.start();
}
}
运行结果:
接收多个客户端发送的请求
2.3.4 基于UDP写一个字典服务器
上面实现的回显服务器缺乏业务逻辑, 这里在上面的代码的基础上稍作调整, 实现一个 “查词典” 的服务器(将英文单词翻译成中文解释), 这里其实就很简单了, 对于客户端的代码还可以继续使用, 服务器只需把处理请求部分的代码修改即可, 我们可以继承上面的回显服务器, 重写请求部分的代码, 英语单词和汉语解释可以由一个哈希表实现映射关系, 构成词库, 然后根据请求来获取哈希表中对应的汉语解释即可
package UDP;
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
/**
* Created with IntelliJ IDEA.
* Description:构建简单翻译服务器
* User: YAO
* Date: 2023-05-29
* Time: 13:16
*/
public class UdpDictServer extends udpServer{
// 1.使用Map数据结构存储需要翻译的词
private Map<String,String> dict = new HashMap<>();
// 2.帮助父类进行构造方法
public UdpDictServer(int port) throws SocketException {
super(port);
dict.put("dog","小狗");
dict.put("cat","小猫");
dict.put("fuck","卧槽");
// .......可以无限的插入数据,跟有道词典相比,咱们的词典就是小.
}
//3.重写process方法,进行根据请求进行计算响应
@Override // 1.提高可读性(用来提示这个方法是进行重写分类的) 2.校验,编译器进行检查
public String process(String request){
return dict.getOrDefault(request,"该单词没有查到");
}
public static void main(String[] args) throws IOException {
UdpDictServer udpDictServer = new UdpDictServer(9090);
udpDictServer.start();
}
}
3. TCP网络编程
TCP相比于UDP有很大的不同, TCP的话首先需要通信双方成功建立连接然后才可以进行通信, TCP进行网络编程的方式和文件读写中的字节流类似, 是字节为单位的流式传输,
对于TCP的套接字, Java提供了两个类来进行数据的传输, 一个是ServerSocket, 是专门给服务器使用的Socket对象, 用来让服务器接收客户端的连接;第二个是Socket, 这个类在客户端和服务器都会使用, 进行服务器与客户端之间的数据传输通信, TCP的传输可以类比打电话的场景, 客户端发送请求后, 服务器调用ServerSocket类的accept方法来 “建立连接” (接通电话), 建立连接后两端就可以进行通信了, Socket可以获取到文件(网卡)的输入输出流对象, 然后就可以流对象进行文件(网卡)读写了, 体现了TCP面向字节流, 全双工的特点.
3.1 ServerSocketAPI
3.2 SocketAPI
3.3 基于TCP实现的回显服务器
3.3.1 TCP服务器实现步骤
1. 创建ServerSocket实例对象,需要指定服务器的端口
2. 启动服务器,使用accept方法和客户端进行连接,如果没有客户端进行连接,此时的accept就会进入堵塞等待
3. 接受客户端发送的请求(通过Socket获取输入流InputStream流对象来读取请求)
4. 处理客户端请求进行计算响应
5. 将响应通过客户端(通过Socket获取到OutputStream流对象发送响应)
3.3.2 TCP服务器代码
package TCP;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Created with IntelliJ IDEA.
* Description:
* User: YAO
* Date: 2023-05-31
* Time: 15:09
*/
public class tcpServerFinal {
private ServerSocket socket = null;
public tcpServerFinal(int port) throws IOException {
socket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
// 创建线程池用来使用多线程进行处理多个客户端
ExecutorService pool = Executors.newCachedThreadPool();
while (true){
// 接收客户端的连接
Socket clientSocket = socket.accept();
pool.submit(new Runnable() {
@Override
public void run() {
// 处理客户端的连接
processConnection(clientSocket );
}
});
}
}
private void processConnection(Socket clientSocket ) {
System.out.printf("[%s:%d]客户端上线!\n",clientSocket.getInetAddress(),clientSocket.getPort());
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()){
// 将字节流转换成字符流进行操作
// 1. 输入(接受客户端的输入)
Scanner scanner = new Scanner(inputStream);
// 2.输出(写入客户端)
PrintWriter printWriter = new PrintWriter(outputStream);
while (true){
// 1.读取请求
if(!scanner.hasNext()){
System.out.printf("[%s:%d] 客户端下线\n", clientSocket.getInetAddress(),clientSocket.getPort());
break;
}
// 直接使用scanner进行读取字符串
String request = scanner.next();
// 2.根据请求进行计算响应
String response = process(request);
// 3.将响应返回给客户端 // 这里println发送是带有换行的
printWriter.println(response);
// 由于缓冲区的存在这里我们需要自己手动进行刷新,强行将缓冲池里的东西写入到客户端
printWriter.flush();
System.out.printf("[%s:%d] req: %s; resp: %s\n",clientSocket.getInetAddress(),clientSocket.getPort(),request,response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
tcpServerFinal tcpServerFinal = new tcpServerFinal(9090);
tcpServerFinal.start();
}
}
3.3.3 TCP客户端代码
package TCP;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
/**
* Created with IntelliJ IDEA.
* Description:
* User: YAO
* Date: 2023-06-01
* Time: 22:29
*/
public class tcpClientFinal {
private Socket socket = null;
public tcpClientFinal(String serverIP, int serverPort) throws IOException {
socket = new Socket(serverIP,serverPort);
}
public void start(){
Scanner scanner = new Scanner(System.in);
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
PrintWriter printWriter = new PrintWriter(outputStream);
Scanner scannerFromSocket = new Scanner(inputStream);
while (true){
System.out.println("->: ");
String request = scanner.next();
printWriter.println(request);
// 由于缓冲区的存在这里我们需要自己手动进行刷新,强行将缓冲池里的东西写入到服务器
printWriter.flush();
String response = scannerFromSocket.next();
System.out.printf("req: %s; resp: %s\n", request,response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
tcpClientFinal tcpClientFinal = new tcpClientFinal("127.0.0.1",9090);
tcpClientFinal.start();
}
}
3.3.4 TCP实现查词典的服务器
package TCP;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* Created with IntelliJ IDEA.
* Description:
* User: YAO
* Date: 2023-06-05
* Time: 15:11
*/
public class tcpServerDict extends tcpServerFinal{
Map<String,String> dict = new HashMap<>();
public tcpServerDict(int port) throws IOException {
super(port);
dict.put("cat", "小猫");
dict.put("dog", "小狗");
dict.put("bird", "小鸟");
}
@Override
public String process(String request){
return dict.getOrDefault(request,"当前单词没有查到结果");
}
public static void main(String[] args) throws IOException {
tcpServerDict tcpServerDict = new tcpServerDict(9090);
tcpServerDict.start();
}
}