JavaEE 网络编程
文章目录
- JavaEE 网络编程
- 引子
- 1. 网络编程-相关概念
- 1.1 基本概念
- 1.2 发送端和接收端
- 1.3 请求和响应
- 1.4 客户端和服务端
- 2. Socket 套接字
- 2.1 数据包套接字通信模型
- 2.2 流套接字通信模型
- 2.3 Socket编程注意事项
- 3. UDP数据报套接字编程
- 3.1 DatagramSocket
- 3.2 DatagramPacket
- 4. TCP流套接字编程
- 4.1 ServerSocket
- 4.2 Socket
引子
如今,我们在任意浏览器打开在线视频网站,如b站去看视频,实质上便是通过网络来获取网络上的视频资源:
与本地打开视频文件类似,只是当前的视频文件资源的来源是网络,相比于本地资源来说,网络提供了更为丰富的网络资源:
所谓的网络资源,其实就是在网络中可以获取的各种数据资源。
而所有的网络资源,都是通过网络编程来进行数据传输的。
1. 网络编程-相关概念
1.1 基本概念
网络编程,指网络上的主机,通过不同的进程,以编程的方式实现网络通信(或称为网络数据传输)
网络编程的目的便是提供网络上不同的主机基于网络来传输数据资源,如图:
进程A:编程来获取网络资源;进程B:编程来提供网络资源
1.2 发送端和接收端
在一次网络数据传输时:
-
发送端: 数据的发送方进程,称为发送端。发送端主机即网络通信中的源主机;
-
接收端: 数据的接受方进程,称为接收端。接收端主机即网络通信中的目的主机;
-
收发端: 发送端和接收端两端,也简称收发端;
注:发送端和接收端只是相对的,只是一次网络数据传输产生数据流向后的概念。
1.3 请求和响应
一般来说,获取一个网络资源,涉及到两次网络数据传输:
- 第一次:请求数据的发送;
- 第二次:响应数据的发送;
好比在饭店点一份炒饭,先要发起请求:点一份炒饭,再有饭店提供对应的响应:提供一份炒饭
1.4 客户端和服务端
- 服务端: 在常见的网络数据传输场景下,把提供服务的一方进程,称为服务端,可以提供对外服务;
- 客户端: 获取服务的一方进程,称为客户端;
一般的客户端服务端模型提供以下操作:
-
客户端获取服务资源
-
客户端保存资源在服务端
最常见的模型是,客户端是指给用户使用的程序,服务端是提供用户服务的程序:
- 客户端先发送请求到服务端;
- 服务端根据请求数据,执行相应的业务处理;
- 服务端返回响应:发送业务处理结果;
- 客户端根据响应数据,展示处理结果(展示获取的资源,或提示保存数据资源的处理结果)
2. Socket 套接字
Socket套接字,是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。基于Socket套接字的网络程序开发就是网络编程
Socket套接字主要针对传输层协议分为如下三类:
数据报套接字: 使用传输层UDP协议
UDP,即User Datagram Protocol (用户数据报协议),传输层协议。
特点:
- 无连接;
- 不可靠传输;
- 面向数据报;
- 有接收缓冲区,无发送缓冲区;
- 大小受限:一次最多传输64K;
对于数据报来说,可以简单理解为,传输数据是一块一块的,发送一块数据假如要100个字节,必须一次发送,接收也必须一次接收100个字节,而不能分100次,每次接收1个字节。
流套接字: 使用传输层TCP协议
TCP,即Transmission Control Protocol (传输控制协议),传输层协议。
特点:
- 有连接;
- 可靠传输;
- 面向字节流;
- 有接收缓冲区,也有发送缓冲区;
- 大小不限;
对于字节流来说,可以简单理解为,传输数据是基于IO流的,流式数据的特征就是在IO流没有关闭的情况下,是无边界的数据,可以多次发送,也可以分开多次接收。
原始套接字: 用于自定义传输层协议,用于读写内核没有处理的IP协议数据(了解)。
2.1 数据包套接字通信模型
对于UDP协议来说,具有无连接,面向数据报的特征,即每次都是没有建立连接,并且一次发送全部数据报,一次接收全部的数据报。
Java中使用UDP协议通信,主要基于 DatagramSocket
类来创建数据包套接字,并使用 DatagramPacket
作为发送或接收的UDP数据报。
一般发送及接收UDP数据报的流程如下:
2.2 流套接字通信模型
对于TCP协议来说,其具有连接且面向字节流的特征,在进行网络通信前需要通过accept()
接收连接请求建立连接后才可进行通信:
2.3 Socket编程注意事项
-
客户端和服务端:开发时,经常是基于一个主机开启两个进程作为客户端和服务端,但真实的场景一般是不同主机;
-
需要注意目的IP和目的端口号,标识了一次数据传输时要发送数据的终点主机和进程
-
如果一个进程A已经绑定了一个端口,再启动一个进程B绑定该端口,就会报错,这种情况也叫端口被占用。对于Java进程来说,端口被占用常见报错信息如下:
对此,如果占用端口的进程A不需要运行,可以关闭A后再启动需要绑定该端口的进程B;如果需要运行A进程,则可以修改进程B的绑定端口,换为其它没有使用的端口。
3. UDP数据报套接字编程
以下是UDP数据报编程相关的API:
3.1 DatagramSocket
DatagramSocket
是UDP Socket 用于发送和接收UDP的数据报。
DatagramSocket
构造方法
方法签名 | 方法说明 |
---|---|
DatagramSocket() | 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口(一般用于客户端) |
DatagramSocket(int port) | 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务端) |
DatagramSocket
方法
方法签名 | 方法说明 |
---|---|
void receive(DatagramPacket p) | 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待) |
void send(DatagramPacket p) | 从此套接字发送数据报(不会阻塞等待,直接发送) |
void close() | 关闭此数据报套接字 |
3.2 DatagramPacket
DatagramPacket是UDP Socket发送和接收的数据报,传递的数据内容参数都存储在由它构建的对象中。
DatagramPacket
构造方法
方法签名 | 方法说明 |
---|---|
DatagramPacket(byte[] buf, int length) | 构造一个DatagramPacket用来接收数据报,接收的数据报保存在字节数组(第一个参数buf)中,接收指定长度(第二个参数length) |
DatagramPacket(byte[] offset, int length, SocketAddress address) | 构造一个DatagramPacker用来发送数据报,发送的数据为字节数组(第一个参数buff)中,从0到指定长度(第二个参数length)。address指定目的主机的IP和端口号 |
DatagramPacket
方法
方法签名 | 方法说明 |
---|---|
InetAddress getAddress() | 从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,和获取接收端主机IP地址 |
int getPort() | 从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号 |
byte[] getData() | 获取数据报中的数据 |
在UDP客户端发送数据报时,需要传入SocketAddress
,该对象可以使用InetSocketAddress
来创建
方法签名 | 方法说明 |
---|---|
InetSocketAddress(InetAddress addr, int port) | 创建一个Socket地址,包含IP地址和端口号 |
代码示例
UDP Echo Server(服务器)
package netWork;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
/* 回显服务器 */
public class UdpEchoServer {
private DatagramSocket socket = null;
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
// 每次循环,就是处理一个请求-响应的过程
// 1. 读取请求并解析
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(requestPacket); // 在客户端没有请求时此处会进入阻塞
// 读到的字节数组,转成String方便后续的逻辑处理
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
// 2. 根据请求计算响应 (对于 回显服务器 来说,这一步啥都不用做)
String response = process(request);
// 3. 把响应返回给客户端
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);
}
}
// 根据请求计算响应,,由于是回显程序,响应内容和请求完全一样
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer server = new UdpEchoServer(9090);
server.start();
}
}
UDP Echo Client(客户端)
package netWork;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
/* 客户端 */
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIp;
private int serverPort;
// 此处ip使用的字符串,点分十进制风格 "192.168.2.100"
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) {
System.out.print("->"); // 提示用户接下来要输入内容
// 1. 从控制台读取要发送的请求数据
if (!scanner.hasNext()) {
break;
}
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[4096], 4096);
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", 9090);
client.start();
}
}
UDP Dict Server(字典服务器)
编写一个英译汉的服务器,只需要重写process
package netWork;
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
public class UdpDictServer extends UdpEchoServer{
HashMap<String, String> map = new HashMap<>();
public UdpDictServer(int port) throws SocketException {
super(port);
map.put("cat", "猫");
map.put("dog", "狗");
map.put("apple", "苹果");
}
@Override
public String process(String request) {
return map.getOrDefault(request, "所查单词不存在!");
}
public static void main(String[] args) throws IOException {
UdpDictServer server = new UdpDictServer(9090);
server.start();
}
}
注:因为此处使用的字典服务器与上述回显服务器端口号相同,如果同时启动会报错,因此需要关闭其中一个服务器才能使用!
4. TCP流套接字编程
以下是TCP流编程相关的API:
4.1 ServerSocket
ServerSocket
是创建TCP服务端Socket的API
ServerSocket
构造方法
方法签名 | 方法说明 |
---|---|
ServerSocket(int port) | 创建一个服务端流套接字Socket,并绑定到指定端口 |
ServerSocket
方法
方法签名 | 方法说明 |
---|---|
Socket accept() | 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待 |
void close() | 关闭此套接字 |
4.2 Socket
Socket
有两种存在形式:
- 一种是客户端Socket
- 另一种是服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。
Socket
构造方法
方法签名 | 方法说明 |
---|---|
Socket(String host, int port) | 创建一个客户端流套接字Socket,并于对应IP的主机上,对应端口的进程建立连接 |
Socket
方法
方法签名 | 方法说明 |
---|---|
InetAddress getInetAddress() | 返回套接字所连接的地址 |
InputStream getInputStream() | 返回此套接字的输入流 |
OutputStream getOutputStream() | 返回此套接字的输出流 |
代码示例
TCP Echo Server(服务器)
package netWork;
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;
/*TCP 回显服务器*/
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("服务器启动!");
while(true) {
// 通过 accept 方法来“接听电话”,然后才能进行通信
Socket clientSocket = serverSocket.accept(); // 在未接收到客户端连接请求前到此处将处于阻塞状态
processConnection(clientSocket);
}
}
// 通过这个方法来处理一次连接,连接建立的过程中就会涉及到多次的请求响应交互
public void processConnection(Socket clientSocket) {
System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
// 循环的读取客户端的请求并返回响应
try (InputStream inputStream = clientSocket.getInputStream(); OutputStream outputStream = clientSocket.getOutputStream()) {
while(true) {
Scanner scanner = new Scanner(inputStream);
if (!scanner.hasNext()) {
// 读取完毕,客户端断开连接,就会产生读取完成!
System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
break;
}
// 1. 读取请求并解析,这里注意隐藏的约定,next 读的时候要读到空白符才会解释
// 因此要求客户端发来的请求必须带有空白符结尾,比如 \n 或者空格
String request = scanner.next();
// 2. 根据请求计算响应
String response = process(request);
// 3. 把响应返回给客户端
// 通过这种方式可以写回,但是这种方式不方便给返回的响应中添加 \n
//outputStream.write(response.getBytes(), 0, response.getBytes().length);
// 可以给 outputStream 套上一层,完成更方便的写入
PrintWriter printWriter = new PrintWriter(outputStream);
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);
}
finally {
try {
clientSocket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
TCP Echo Client(客户端)
package netWork;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(String serverIp, int serverPort) throws IOException {
// 此处可以把这里的 ip 和 port 直接传给socket对象
// 因为此处 tcp 是有连接的,因此 socket 里面就会保存好这两信息
// 因此此处 TcpEchoClient 类就不必保存
socket = new Socket(serverIp, serverPort);
}
public void start() throws IOException {
System.out.println("客户端启动!");
try (InputStream inputStream = socket.getInputStream();OutputStream outputStream = socket.getOutputStream()) {
Scanner scannerConsole = new Scanner(System.in);
Scanner scannerNetWork = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
while (true) {
System.out.print("->");
// 1. 从控制台读取输入的字符串
if (!scannerConsole.hasNext()) {
break;
}
String request = scannerConsole.next();
// 2. 将请求发送给服务器
// 这里需要使用 println 来发送,为了让发送的请求末尾带有 \n , 这里与服务器的 scanner.next 相呼应
printWriter.println(request);
// 通过这个 flush 主动刷新缓冲区,确保数据真的发出去了
printWriter.flush();
// 3. 从服务器中接受响应
String response = scannerNetWork.next();
// 4. 把响应显示出来
System.out.println(response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.7", 9090);
client.start();
}
}
为了能够响应多个客户端请求,可以进行以下修改:
注:如果想在IDEA中同时打开多个相同的客户端,可以按一下步骤修改设置:
-
给每个客户端都分配一个线程
public void start() throws IOException { System.out.println("服务器启动!"); while(true) { // 通过 accept 方法来“接听电话”,然后才能进行通信 Socket clientSocket = serverSocket.accept(); // processConnection(clientSocket); Thread t = new Thread(() -> { processConnection(clientSocket); }); t.start(); } }
-
为了避免频繁创建销毁线程,可以引入线程池
public void start() throws IOException { System.out.println("服务器启动!"); // 可通过调用线程池的方式完成多个客户端的同时连接 ExecutorService pool = Executors.newCachedThreadPool(); while(true) { // 通过 accept 方法来“接听电话”,然后才能进行通信 Socket clientSocket = serverSocket.accept(); pool.submit(new Runnable() { @Override public void run() { processConnection(clientSocket); } }); } }