网络编程
什么是网络编程
网络编程,指网络上的主机,通过不同的进程,以编程的方式实现网络通信(或称为网络数据传输)
Socket套接字
网络编程的核心就是 Socket API (操作系统给应用程序提供网络编程的 API)
可以认为是Socket API 是跟传输层密切相关的, 传输层里提供了最核心的两种协议, (UDP, TCP), 因此Socket API 也提供了两种风格(UDP,TCP)
简单认识UDP和TCP
UDP: 无连接, 不可靠传输, 面向数据报, 全双工
TCP: 有连接, 可靠传输, 面向字节流, 全双工
有连接就是类似于打电话这种, 需要建立连接才能通信, 建立连接需要对方接受, 如果连接没有建立好就通信不了无连接就像是发短信/发微信这种, 直接发就行了
什么是可靠不可靠传输呢?网络环境非常复杂, 不能保证所发送的数据100%到达
发送方能知道自己发的数据是发过去了, 还是丢了(打电话时=是可靠传输, 发短信发微信是不可靠传输,但是带已读功能的就可以认为是可靠传输), 可靠不可靠跟有连接还是无连接时没有关系的
面向字节流: 数据传输和文件读取类似于"流式"的面向数据报: 数据传输以一个个"数据报"为基本单位(一个数据报可能是若干个字节, 带有一定的格式)
全双工就是一个通信通道可以双向传输, (既可以发送, 也可以接收)
基于 UDP 编写一个客户端服务器网络通信程序
DatagramSocket
使用这个类来表示 Socket 对象, 在操作系统中, 把这个Socket对象也是当成一个文件来处理, 相当于是文件描述符上的一项
普通的文件, 对应的硬件设备是硬盘,
Socket 文件, 对应的硬件设备是网卡
一个 Socket 对象, 就可以和另外一个主机通信了. 要想和多个不同的主机进行通信就需要创建多个 Socket 对象
DatagramSocket 是UDP Socket,用于发送和接收UDP数据报。
DatagramSocket 构造方法:
方法签名 | 方法说明 |
---|---|
DatagramSocket() | 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口(一般用于客户端) |
DatagramSocket(intport) | 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务端) |
上述没有参数的版本, 就是没有指定端口, 系统则会自动分配一个空闲的端口
有参数的版本是要传入一个端口, 此时就是让 Socket 对象和这个端口关联起来,
本质上说, 不是进程和端口之间建立联系, 是进程中的 Socket 对象和端口建立了联系
DatagramSocket 方法:
方法签名 | 方法说明 |
---|---|
void receive(DatagramPacket p) | 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待) |
void send(DatagramPacketp) | 从此套接字发送数据报包(不会阻塞等待,直接发送) |
void close() | 关闭此数据报套接字 |
receive 这个方法此处传入的相当于一个空的对象, receive 方法内部会对这个空对象进行填充, 从而构造出结果数据, 参数也是一个"输出型参数"
close 这个方法是释放资源的, 用完之后进行关闭
DatagramPacket
DatagramPacket 表示 UDP 传输的一个报文
DatagramPacket 构造方法:
方法签名 | 方法说明 |
---|---|
DatagramPacket(byte[] buf, int length) | 构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中,接收指定长度(第二个参数length) |
DatagramPacket(byte[] buf, int offset, int length,SocketAddress address) | 构造一个DatagramPacket以用来发送数据报,发送的数据为字节数组(第一个参数buf)中,从0到指定长度(第二个参数length)。address指定目的主机的IP和端口号 |
编写一个最简单的 UDP 版本的客户端, 服务器程序----回显示服务器(echo server)
一个普通的服务器是: 收到请求, 根据请求计算响应, 返回响应
我们这里省略了计算的过程, 也就是请求什么就返回什么, (这个代码没有什么实际业务, 只是展示一下 Socket API 的用法)
作为一个真正的服务器"根据请求计算响应是及其重要的"
java
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
// UDP 版本的回显服务器
public class UdpEchoServer {
// 网络编程, 本质上是要操作网卡
// 但是网卡不方便直接操作, 在操作系统内核中, 使用"Socket"这样的文件来抽象表示网卡
private DatagramSocket socket = null;
// 对于服务器来说, 创建 Socket 对象的同时, 要让他绑定一个端口号
// 服务器一定要关联一个具体的端口, 服务器是网络传输中被动的一方,
// 如果是操作系统随机分配一个端口, 客户端就不知道这个端口是啥了, 也就无法通信
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
// 服务器不只是只给一个客户端提供服务, 是需要服务很多客户端的
while (true) {
// 只要有客户端过来就提供服务
// 1. 读取服务端发过来的请求
// receive 方法的参数是一个输出型参数, 需要先构造好一个空白的 DatagramPacket 对象, 发给receive 进行填充
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
// receive 内部会针对参数对象进行填充, 填充的数据来自于网卡
socket.receive(requestPacket);
// 此时DatagramPacket 是一个特殊的对象. 不好处理, 可以把这里包含的数据拿出来, 构造一个字符串
// 此处给的最大长度是4096, 但是这里的这些空间不一定用满了, 可能只用了一小部分
// 因此构造字符串的时候, 就通过getLength 来获取实际的数据报长度,
// 把这个实际有小部分构造成字符串即可
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
// 2.根据请求计算响应
String response = process(request);
// 3. 把响应写回客户端, sent 的参数也是DatagramPacket, 需要把Packet对象构造好
// 此处构造的响应对象, 不能用空的字节数组构造, 需要使用响应数据来构造
// 获取到客户端的IP和端口号(这两个信息本来就在 requestPacket中)
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
requestPacket.getSocketAddress());
socket.send(responsePacket);
// 4. 打印日志, 当前这次请求响应的处理中间结果
// 参数分别是 Packet 里的IP, 端口
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 {
// 端口号(1024 ~ 65535)
UdpEchoServer server = new UdpEchoServer(9090);
server.start();
}
}
对于我们这个程序, 这里的while是得一直循环的, 这样的死循环在服务器程序中是没啥问题.一般服务器是7*24 小时运行的
DatagramSocket 这个类的 receive 能阻塞, 会因为操作系统里原生提供的API就是阻塞的函数, 这里的阻塞不是Java实现的, 而是操作系统内核实现的.
系统里对于IO操作本身就有这样的阻塞机制, 哪个线程如果进行IO操作, 在IO完成之前, 就会自动把对应的线程放在阻塞队列中, 暂时不参与调度.
UDP 版本的回显客户端
java
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
// UDP 版本的回显客户端
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIp = null;
private int serverPort = 0;
// 构造这个 socket 对象, 不需要显示绑定一个端口(系统自动分配)
// 一次通信需要两个ip, 两个端口
// 客户端的 ip 是 127.0.0.1
// 服务器 ip 和 端口 也要高数客户端, 才能正确把消息发给服务器
public UdpEchoClient(String serverIp, int serverPort) throws SocketException {
socket = new DatagramSocket();
this.serverIp = serverIp;
this.serverPort = serverPort;
}
public void start() throws IOException {
System.out.println("客户端启动!");
Scanner scanner = new Scanner(System.in);
while (true) {
// 1. 从控制台读取要发送的数据
System.out.print("> ");
String request = scanner.next();
if (request.equals("exit")) {
System.out.println("goodbye");
break;
}
// 2. 构造UDP请求, 发送
// 构造这个Packet的时候, 需要把 serverIp和 port 都传过来, 但是此处IP地址需要填写的是一个32位的整数形式
// 上述的IP地址是一个字符串, 需要使用InetAddress.getByName 来进行一个转换
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
InetAddress.getByName(serverIp),serverPort);
socket.send(requestPacket);
// 3. 读取UDP服务器响应并解析
DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
socket.receive(responsePacket);
String response = new String(responsePacket.getData(),0, responsePacket.getLength());
// 4. 把解析好的结果显示出来
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);
client.start();
}
}
我们在基于上面的代码, 实现一个查词典的功能
java
/**
* @describe
* @author chenhongfei
* @version 1.0
* @date 2023/10/24
*/
package network;
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
// 对于DictServer, 和EchoServer 相比, 大部分东西都是一样的
// 主要是对于根据请求计算响应的代码进行修改
public class UdpDictServer extends UdpEchoServer{
private Map<String,String> dict = new HashMap<>();
public UdpDictServer(int port) throws SocketException {
super(port);
// 给这个dict设置内容
dict.put("dog","狗");
dict.put("cat","猫");
dict.put("hello","你好");
// .....
}
public String process(String request) {
// 查词典的过程
return dict.getOrDefault(request,"当前单词没有查询到结果");
}
public static void main(String[] args) throws IOException {
UdpDictServer server = new UdpDictServer(9090);
server.start();
}
}
如果我们多个客户端都使用同一个端口, 就会出现异常
基本过程:
- 服务器先启动, 运行到 receive 阻塞
- 客户端开始执行, 客户端开始读取用户 输入的内容
- 客户端发送请求
- 客户端在这里 进行阻塞等待服务器响应, 接着服务器接受请求,
- 根据请求计算响应
- 执行send 返回响应,
- 最后客户端从阻塞中返回,读到响应了
TCP流套接字编程
TCP 提供的API主要是两个类
SeverSocket 专门给服务器使用的 Socket 对象
Socket 既可以给客户端使用, 也可以给服务器使用
这在里我们要注意的是: TCP 不需要一个类来表示"TCP 数据报", TCP 不是以数据报为单位来传输的, 是以字节的形式"流式传输"
ServerSocket API
ServerSocket 是创建TCP服务端Socket的API
ServerSocket 构造方法:
方法签名 | 方法说明 |
---|---|
ServerSocket(int port) | 创建一个服务端流套接字Socket,并绑定到指定端口 |
ServerSocket 方法:
方法签名 | 方法说明 |
---|---|
Socketaccept() | 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待 |
void close() | 关闭套接字 |
Socket
在服务器这边, 是由 acecpt 返回的
这客户端这边, 是得由我们构造的, 构造的时候指定一个IP 和 端口号
这里指定的IP 和 端口号 是服务器的IP 和 端口号, 有了这个信心就能和服务器建立连接了
Socket 方法:
方法签名 | 方法说明 |
---|---|
InetAddress getInetAddress() | 返回套接字所连接的地址 |
InputStream getInputStream() | 返回此套接字的输入流 |
OutputStream getOutputStream() | 返回此套接字的输出流 |
TCP 回显服务器
java
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TcpEchoServer {
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动 !");
// 此处使用这个动态变化的线程池
ExecutorService threadPool = Executors.newCachedThreadPool();
while (true) {
// 使用这个clientSocket 和具体的客户端进行交流
Socket clientSocket = serverSocket.accept();
// Thread t = new Thread(() -> {
// 多线程版本
// 这里最大的问题就是频繁的申请释放资源
// try {
// processConnection(clientSocket);
// } catch (IOException e) {
// throw new RuntimeException(e);
// }
// });
// t.start();
// 使用线程池来解决
threadPool.submit(() -> {
try {
processConnection(clientSocket);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
}
// 使用这个方法来处理连接
// 这一个连接对应到一个客户端, 这里可能会涉及到多次交互
private void processConnection(Socket clientSocket) throws IOException {
System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress().toString(),clientSocket.getPort());
// 继续上述的Socket对象和客户端通行
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
// 因为要处理多个请求和响应, 也是要循环来进行
while (true) {
// 1. 读取目录
Scanner scanner = new Scanner(inputStream);
if (!scanner.hasNextInt()) {
// 没有数据了, 就说明读完了(客户端关闭连接)
System.out.printf("[%s:%d]客户端下线\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
break;
}
// 此处使用 next 是一直读取到 换行符/空格/其他空白符结束. 但是最终结果不返回空白符
String request = scanner.next();
// 2. 根据请求构造响应
String response = process(request);
// 3. 返回响应结果
// outputStream 没有write string 这样的功能, 可以把String 里的字节数组拿出来, 进行写入
// 也可以用字符流来转换一下
PrintWriter printWriter = new PrintWriter(outputStream);
// 此处使用 println 来写入, 让结果有一个\n 换行, 方便对端接受解析
printWriter.println(request);
// flush 刷新缓冲区, 保证当前写入的数据, 确实发送出去了
printWriter.flush();
System.out.printf("[%s:%d] req: %s; resp: %s\n",clientSocket.getInetAddress().toString(),clientSocket.getPort(),
request,response);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
clientSocket.close();
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(4090);
server.start();
}
}
这里用到clientSocket , 此时任意一个客户端连接上来, 都会返回或创建一个Socket对象
Socket 就是文件, 每次创建一个clientSocket 对象后, 就会占用一个文件描述符表的位置
所以在使用完之后要进行关闭
TCP 回显客户端
java
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(String serverIp, int serverPort) throws IOException {
// Socket 构造方法, 能够识别点分十进制的IP地址, 比 DatagramPacket 更方便
// new 这个对象的时候就会进行 TCP 连接操作
socket = new Socket(serverIp,serverPort);
}
public void start() {
System.out.println("客户端启动");
Scanner scanner = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
while (true) {
// 1. 先从键盘上读取用户输入的内容
System.out.print("> ");
String request = scanner.next();
if (request.equals("exit")) {
System.out.println("goodbye");
break;
}
// 2. 把读到的内容构成请求, 发送给服务器
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);
// 此处加上close 确保数据发送出去
printWriter.flush();
// 3. 读取服务器的响应
Scanner respScanner = new Scanner(inputStream);
String response = respScanner.next();
// 4. 把响应的内容显示到界面上
System.out.println(request);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1",4090);
client.start();
}
}