目录
一、网络编程
网络编程,指网络主机通过不同的进程,以编程的方式实现网络通信。
【发送端和接收端】
在一次网络数据传输时:
发送端:数据的发送方进程。发送端主机即网络通信中的源主机。
接收端:数据的接收方进程。接收端主机即网络通信中的目的主机。
收发端:发送端和接收端两端,简称为收发端。
注:发送端和接收端只是相对的,只是一次网络数据传输产生数据流向后的概念。
【客户端和服务端】
服务端:提供服务的进程。
客户端:获取服务的进程。
注:客户端获取服务资源,客户端保存资源在服务端。
【请求和响应】
获取一个网络资源,涉及到两次网络数据传输:① 请求数据的发送、② 响应数据的发送。
请求 (request):客户端给服务器发送的数据。
响应 (response):服务器给客户端返回的数据。
二、Socket 套接字
Socket 套接字,系统提供用于网络通信的技术,是基于 TCP/IP 协议的网络通信的基本操作单元。
基于 Socket 套接字的网络程序开发就是网络编程。
【分类】
Socket 套接字主要针对传输层协议划分为三类:
1、数据报套接字:使用传输层 UDP 协议。
UDP,即 User Datagram Protocol (用户数据报协议),传输层协议。
【UDP 特点】
- 无连接
- 不可靠传输
- 面向数据报
- 全双工
- 有接收缓冲区,无发送缓冲区
- 大小受限:一次最多传输 64k
对于数据报来说,数据传输是一块一块的,发送一块数据假如 100 个字节,必须一次发送,接收也必须一次接收 100 个字节,而不能分 100 次,每次接收 1 个字节。
2、流套接字:使用传输层 TCP 协议。
TCP,即 Transmission Control Protocol (传输控制协议),传输层协议。
【TCP 特点】
- 有连接
- 可靠传输
- 面向字节流
- 全双工
- 有接收缓冲区,也有发送缓冲区
- 大小不限
对于字节流来说,传输数据是基于 IO 流,流式数据的特征就是在 IO 流没有关闭的情况下,是无边界的数据,可以发送多次,也可以分开多次接收。
3、原始套接字
原始套接字用于自定义传输层协议,用于读写内核没有处理的 IP 协议数据。
三、UDP 数据报套接字编程
对于 UDP 协议来说,具有无连接、面向数据报的特征,即每次都是没有建立连接,并且一次发送全部数据报,一次接收全部的数据报。
Java 中使用 UDP协议 通信,主要基于 DatagramSocket 类来创建数据报套接字,并使用 DatagramPacket 类作为发送或接收的 UDP 数据报。
1、DatagramSocket
DatagrmSocket 是 UDP Socket 用于发送和接收 UDP 数据报。
【构造方法】
构造方法 | 说明 |
DatagramSocket() | 创建一个 UDP 数据报套接字的 Socket,绑定到本机任意一个随机端口 (一般用于客户端) |
DatagramSocket(int port) | 创建一个 UDP 数据报套接字的 Socket,绑定到本机指定的端口 (一般用于服务端) |
【方法】
方法 | 说明 |
void receive(DatagramPacket p) | 从此套接字接收数据报 (如果没有收到数据报,该方法会阻塞等待) |
void send(DatagramPacket p) | 从此套接字发送数据报 (不会阻塞等待,直接发送) |
void close() | 关闭该数据报套接字 |
2、DatagramPacket
DatagramPacket 是 UDP Socket 发送和接收的 UDP 数据报。
【构造方法】
构造方法 | 说明 |
DatagramPacket(byte[] buf, int length) | 构造一个 DatagramPacket 用于接收数据报,接收的数据保存在字节数组 (参数 buf) 中,接收指定长度 (参数 length) |
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) | 构造一个 DatagramPacket 用于发送数据报,发送的数据保存在字节数组 (参数 buf) 中,从 0 到指定长度 (参数 length),指定目的主机的 IP 和端口号 (参数 address) |
【方法】
方法 | 说明 |
InetAddress getAddress() | 从接收的数据报中,获取发送端主机 IP 地址;或从发送的数据报中,获取接收端主机 IP 地址 |
int getPort() | 从接收的数据报中,获取发送端主机端口号;或从发送的数据报中,获取接收端主机端口号 |
byte[] getData() | 获取数据报中的数据 |
int getLength() | 获取数据报的有效长度 |
3、InetSocketAddress
构造 UDP 发送的数据报时,需要传入 SocketAddress,该对象可以使用 InetSocketAddress 来创建。(InetSocketAddress 是 SocketAddress 的子类)
【构造方法】
构造方法 | 说明 |
InetSocketAddress(InetSocketAddress addr, int port) | 创建一个 Socket 地址,包含 IP 地址和端口号 |
【UDP编程案例】
通过UDP服务器与UDP客户端的交互,实现客户端输入请求,服务器返回响应的功能。
【UDP回显服务器】
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UdpEchoServer {
//1、创建 DatagramSocket 对象
private DatagramSocket socket;
//一个端口只能被一个进程绑定,一个进程可以绑定多个端口
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,方便后续的逻辑处理
//从字节数组的下标 0 位置开始构造 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], request: %s, response: %s", requestPacket.getAddress().toString(),
requestPacket.getPort(), request, response);
}
}
//根据需求编写 process 方法
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
//给客户端提供端口号
UdpEchoServer server = new UdpEchoServer(9090);
server.start();
}
}
【UDP回显客户端】
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket;
private String serverIp;
private int serverPort;
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 {
//通过 IP 地址和端口号找到服务器
UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);
client.start();
}
}
【总结】
1、服务器启动后,在 receive 处阻塞,等待客户端 send 请求。
2、客户端在 scanner.hasNext 处等待控制台输入,控制台输入后,构造请求并 send,然后在 receive 处阻塞,等待响应。
3、服务器 receive 请求后,进行 process 处理,构造响应并 send,然后进行下一次循环,继续在 receive 处阻塞等待下一次请求。
4、客户端 receive 响应并打印在控制台后,进行下一次循环,等待控制台输入。
【构造 DatagramPacket 对象】
1、构造时指定空白字节数组。(搭配 receive 使用)
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
2、构造时指定有内容的数组,并指定 IP 和端口。(搭配 send 使用)
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),
response.getBytes().length, requestPacket.getSocketAddress());
3、构造时指定有内容的数组,并分开指定 IP 和端口。(搭配 send 使用)
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),
request.getBytes().length,InetAddress.getByName(serverIp), serverPort);
【简易英译汉词典】
继承 UdpEchoServer 类,重写 process 方法,实现简易的英译汉需求。
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
public class UdpDictServer extends UdpEchoServer{
//构造一个哈希表
private HashMap<String, String> hashMap = new HashMap<>();
public UdpDictServer(int port) throws SocketException {
super(port);
//设置键值对 (实际词典就是一个巨大的 hashMap)
hashMap.put("moon", "月亮");
//后续可以添加无数个英汉键值对
}
@Override
public String process(String request) {
return hashMap.getOrDefault(request, "词典中没有找到该单词");
}
public static void main(String[] args) throws IOException {
UdpDictServer server = new UdpDictServer(9090);
server.start();
}
}
四、TCP 流套接字编程
1、ServerSocket
ServerSocket 是创建 TCP 服务器 Socket 的 API。
【构造方法】
构造方法 | 说明 |
ServerSocket(int port) | 创建一个服务器流套接字 Socket,并绑定到指定端口 |
【方法】
方法 | 说明 |
Socket accept() | 开始监听指定端口,有客户端连接后,返回一个服务器 Socket 对象,并基于该 Socket 建立与客户端的连接,否则阻塞等待 |
void close() | 关闭此套接字 |
2、Socket
Socket 是客户端 Socket,或服务器中接收到客户端建立连接 (accepy 方法) 的请求后,返回的服务器 Socket。
不管是客户端还是服务器 Socket,都是双方建立连接以后,用于保存对方信息,与对方收发数据。
【构造方法】
构造方法 | 说明 |
Socket(String host, int port) | 创建一个客户端流套接字 Socket,并与对应 IP 的主机上对应端口的进程进行连接。 |
【方法】
方法 | 说明 |
InetAddress getInetAddress() | 返回套接字所连接的地址 |
InputStream getInputStream() | 返回此套接字的输入流 |
OutputStream getOutputStream() | 返回此套接字的输出流 |
【TCP编程案例】
通过TCP服务器与TCP客户端的交互,实现客户端输入请求,服务器返回响应的功能。
【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;
public class TcpEchoServer {
private ServerSocket serverSocket;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("启动服务器");
while (true) {
//接收客户端发起的连接请求
Socket clientSocket = serverSocket.accept();
//处理连接
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(); //写入
Scanner scanner = new Scanner(inputStream); //包装输入流
PrintWriter printWriter = new PrintWriter(outputStream); //包装输出流
while (true) {
//scanner 读取网络请求
if (!scanner.hasNext()) {
//客户端断开连接,读取结束
System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
break;
}
//1、获取请求
//next 读到空白符才会结束,因此客户端的请求必须带有 \n 或空格
String request = scanner.next();
//2、构造响应
String response = processResponse(request);
//3、返回响应给客户端
printWriter.println(response);
//刷新 OutputStream 的缓冲区
printWriter.flush();
System.out.printf("[%s:%d], request: %s, response: %s\n", clientSocket.getInetAddress(), clientSocket.getPort(), request, response);
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
//释放 clientSocket,由于每个客户端都有一个 clientSocket,
//若不释放,客户端越来越多,将会把文件描述符表占满
try {
clientSocket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
private String processResponse(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
【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;
public class TcpEchoClient {
private Socket socket;
public TcpEchoClient(String serverIp, int serverPort) throws IOException {
//TCP 是有连接的,故 TCP socket 会保存对端的信息
socket = new Socket(serverIp, serverPort); //和对应的服务器建立连接 (系统内核中完成)
}
public void start() {
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、将请求发给服务器
//使用 PrintWriter 方便请求末尾带有 \n
printWriter.println(request);
//刷新 OutputStream 的缓冲区
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.1", 9090);
client.start();
}
}
【总结】
1、服务端在 accept 处阻塞等待客户端建立连接。
2、构造客户端对象的 new 操作触发了客户端与服务端建立连接请求,即"三次握手"。
3、服务器 accept 客户端连接后,进入 processConnection 方法处理连接,在 hasNext 处阻塞等待客户端请求。
4、客户端在 hasNext 处等待控制台输入,控制台输入后,构造请求并写入服务器,然后等待服务器响应。
5、服务器读取到请求后,构造响应并写回客户端。
6、客户端读取响应,并打印至控制台。
【如何运行多个实例】
打开第 ① 步中 Edit Configurations,第 ② 步找到要设置的类,点击 Modify options,第 ③ 步将 Allow multiple instances 勾上。
【解决TCP回显服务器不能支持多个客户端同时访问的问题】
在上述TCP回显服务器代码中,有一个明显的缺陷,即第一个客户端工作时,第二个客户端没有任何响应;当第一个客户端断开连接,第二个客户端才能正常工作。此时我们就可以引入线程池来解决该问题,每次来一个客户端,就用应该新线程来执行。
public void start() throws IOException {
System.out.println("启动服务器");
//创建线程池实例
ExecutorService pool = Executors.newCachedThreadPool();
while (true) {
Socket clientSocket = serverSocket.accept();
//给线程池添加任务
pool.submit(new Runnable() {
@Override
public void run() {
processConnection(clientSocket);
}
});
}
}
总结
1、网络编程,指网络主机通过不同的进程,以编程的方式实现网络通信。
2、DatagrmSocket 用于发送和接收 UDP 数据报,DatagramPacket 是 UDP 数据报。
3、网络连接本质上就是通信双方各自保存对方的信息。