1. 什么是网络编程
网络编程是指网络上的主机,通过不同的进程,以编程的方式实现 网络通信(或称为网络数据传输)
只要满足不同的进程就可以进行通信,所以即便是在同一个主机,只要不同的进程,基于网络传输数据,也属于网络编程
2. 网络编程中的基本概念
1)发送端和接收端
在一次网络传输中:
发送端: 数据的 发送方进程,称为发送端。发送端主机即网络通信中的源主机
接收端: 数据的 接收方进程,称为接收端。接收端主机即网络通信中的目的主机
收发端: 发送端和接收端两端,简称为收发端
注意: 发送端和接收端只是相对的,只是一次网络数据传输产生数据流向后的概念
2)请求和响应
一般来说,获取一个网络资源时,涉及到两次网络数据传输
- 第一次:请求数据的发送
- 第二次:响应数据的发送
就好比在饭店里点了一碗面
顾客先发起请求:来一碗面
饭店再做出响应:提供一碗面
3)客户端和服务端
服务端: 在常见的网络传输场景下,把 提供服务 的一方进程,称为服务端,可以对外提供服务
客户端:获取服务 的一方进程,称为客户端
对于服务来说,一般是提供:
- 客户端获取服务资源
- 客户端保存资源到服务端
4)常见的客户端服务端模型
在常见的场景中,客户端是指给用户使用的程序,服务端是指提供用户服务的程序,具体流程如下:
- 客户端发送请求到服务端
- 服务端根据请求数据,执行相应的业务处理
- 服务端返回响应:发送业务处理结果
- 客户端根据响应数据,展示处理结果
3. Socket 套接字
Socket 套接字时由系统提供用于网络通信的技术,是基于 TCP/IP 协议的网络通信的基本操作单元。基于 Socket 套接字的网络程序开发就是网络编程
1)Socket 的分类
常用的 Socket 套接字有以下两类:
流套接字: 使用 TCP 协议
传输数据基于 IO 流,流式数据的特征就是在 IO 流没有关闭的情况下,是无边界的数据,可以多次发送数据,也可以分开多次接收
数据报套接字: 使用 UDP 协议
传输数据是一块一块的,每一块数据必须一次性发送,接受也必须一次性接受,不能分开发送或接收
2)Java 数据报套接字通信模型
UDP 协议具有无连接、面向数据报的特征,即每次传输都是没有建立连接,并且一次发送全部数据,一次接收全部数据
Java 中使用 UDP 协议通信时,主要基于 DatagramSocket 类来创建数据报套接字,并使用 DatagramPacket 作为发送或者接受的数据报。
具体流程如下:
3)Java 流套接字通信模型
TCP 协议具有有连接、面向字节流的特征,即传输数据之前要先建立连接,并且数据的传输是基于 IO 流的
具体流程如下:
4. UDP 数据报套接字编程
1)DatagramSocket API
DatagramSocket 是 UDP Socket,用于发送和接收 UDP 数据报
构造方法:
实例方法:
2)DatagramPacket API
DatagramPacket 是 UDP Socket 发送和接收的数据报
构造方法:
实例方法:
3)示例
使用 UDP Socket 套接字完成一个简单的翻译小程序
客户端向服务端发起翻译请求,服务端接收到请求后,对请求进行处理,再向客户端做出响应
服务端
服务端接收到请求之后,在我们人为规定的 map 中进行查询,再对客户端做出响应
public class Server { private final static HashMap<String, String> map = new HashMap<>(); static { map.put("苹果", "apple"); map.put("香蕉", "banana"); map.put("梨", "pear"); map.put("电脑", "computer"); map.put("手机", "phone"); } public static void main(String[] args) throws IOException { // 得到 socket 对象,并对其绑定端口号 Log.println("服务器开启并且绑定了 8080 端口"); DatagramSocket socket = new DatagramSocket(8080); // 循环接受和处理请求 while (true) { // 准备接收数据的容器 byte[] buf = new byte[1024]; // 包装数据包 DatagramPacket received = new DatagramPacket(buf, 0, 1024); // 接收请求 Log.println("服务器准备接受请求"); socket.receive(received); Log.println("服务器接受到了请求"); // 处理请求 InetAddress address = received.getAddress(); // 得到发起请求方的 IP Log.println("对方的地址:" + address); int port = received.getPort(); // 得到发起请求方的端口号 Log.println("对方的端口:" + port); int length = received.getLength(); // 得到请求中真实有效的数据长度 Log.println("真实的数据长度:" + length); byte[] realData = Arrays.copyOf(buf, length); // 得到请求中真实有效的数据 // 将请求数据转换成 String String request = new String(realData, 0, length, StandardCharsets.UTF_8); Log.println("请求中的数据:\r\n" + request); // 人为规定请求和相应的格式 // 请求格式必须以 “我是请求:\r\n” 开始,以 “\r\n” 结束 if (!request.startsWith("我是请求:\r\n")) { Log.println("请求格式出错"); // 请求出错 continue; } if (!request.endsWith("\r\n")) { Log.println("请求格式出错"); // 请求出错 continue; } // 得到请求中的英文单词 String EnglishWord = request.substring("我是请求:\r\n".length(), request.length() - 2); Log.println("请求中的英文单词:" + EnglishWord); // 进行翻译 String ChineseWord = map.getOrDefault(EnglishWord, "我不知道"); Log.println("翻译后的中文:" + ChineseWord); // 将翻译后的结果进行包装 String response = String.format(ChineseWord, "%s\r\n"); byte[] bytes = response.getBytes(StandardCharsets.UTF_8); // 包装数据包 DatagramPacket send = new DatagramPacket(bytes, 0, bytes.length, address, port); // 发起响应 Log.println("准备发送响应"); socket.send(send); Log.println("相应发送成功"); } } }
客户端
public class Client { public static void main(String[] args) throws IOException { Scanner sc = new Scanner(System.in); DatagramSocket socket = new DatagramSocket(9999); while (sc.hasNextLine()) { String str = sc.nextLine(); String send = "我是请求:\r\n" + str + "\r\n"; byte[] bytes = send.getBytes(); DatagramPacket request = new DatagramPacket(bytes, 0, bytes.length, InetAddress.getLoopbackAddress(), 8080); socket.send(request); // 接收响应 byte[] buf = new byte[1024]; DatagramPacket datagramPacket = new DatagramPacket(buf, 0, 1024); socket.receive(datagramPacket); int n = datagramPacket.getLength(); String response = new String(buf, 0, n, StandardCharsets.UTF_8); Log.println(response); } } }
5. TCP 流套接字编程
1)ServerSocket API
用于创建 TCP 服务端 Socket 的 API
构造方法:
方法签名 | 方法说明 |
ServerSocket(int port) | 创建一个服务端流套接字 Socket,并绑定到指定端口 |
实例方法:
方法签名 | 方法说明 |
Socket accept() | 等待客户端发起连接,连接成功后返回一个 Socket 对象 |
void close() | 关闭 Socket |
2)Socket API
这里的 Socket 是客户端 Socket,或服务端中接收到客户端连接请求后,accept 方法返回的服务端 Socket
不管是客户端还是服务端 Socket,都是双方建立连接后,保存对端信息,以及用来与对方收发数据的
构造方法:
方法签名 | 方法说明 |
Socket(String host, int port) | 创建一个客户端流套接字,并与对应 IP 的主机,对应端口的进程建立连接 |
实例方法:
方法签名 | 方法说明 |
InetAddress getInetAddress() | 返回套接字所连接的 IP 地址 |
InputStream getInputStream() | 返回套接字的输入流 |
OutputStream getOutputStream() | 返回套接字的输出流 |
3)示例
使用 TCP Socket 套接字完成一个简单的翻译小程序
完成的效果和上文 UDP 的小程序效果一样
不过在 TCP 这里有长短连接的区分,所以有两个版本的代码,在长连接版本中加入了并发编程,使得同一个服务端可以被多个客户端所连接
a. 短连接版本
客户端:
public class 短连接版本Client { public static void main(String[] args) throws IOException { for (int i = 0; i < 3; i++) { Socket socket = new Socket("127.0.0.1", 8080); // 拨号 OutputStream os = socket.getOutputStream(); OutputStreamWriter writer = new OutputStreamWriter(os, "UTF-8"); PrintWriter printWriter = new PrintWriter(writer); printWriter.printf("我是请求\r\n%s\r\n", "苹果"); printWriter.flush(); InputStream is = socket.getInputStream(); Scanner scanner = new Scanner(is, "UTF-8"); String header = scanner.nextLine(); String word = scanner.nextLine(); System.out.println(word); socket.close(); // 挂电话 } } }
服务端:
public class 短连接版本Server { private final static HashMap<String, String> map = new HashMap<>(); static { map.put("苹果", "apple"); map.put("香蕉", "banana"); map.put("梨", "pear"); map.put("电脑", "computer"); map.put("手机", "phone"); } public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(8080); while(true) { Log.println("等待客户端建立连接"); Socket socket = serverSocket.accept(); Log.println("客户端连接成功"); InetAddress address = socket.getInetAddress(); Log.println("客户端 IP 地址:" + address); int port = socket.getPort(); Log.println("客户端端口号:" + port); InputStream is = socket.getInputStream(); Scanner sc = new Scanner(is, "UTF-8"); String header = sc.nextLine(); String EnglishWord = sc.nextLine(); Log.println("请求中的英文单词为:" + EnglishWord); String ChineseWord = map.getOrDefault(EnglishWord, "我不知道"); Log.println("翻译后的中文为:" + ChineseWord); Log.println("准备响应"); OutputStream os = socket.getOutputStream(); OutputStreamWriter writer = new OutputStreamWriter(os); PrintWriter printWriter = new PrintWriter(writer); printWriter.printf("OK!\r\n%s\t\n", ChineseWord); printWriter.flush(); Log.println("响应成功"); socket.close(); Log.println("断开连接"); } } }
b. 长连接并发版本
在长连接版本中,必须服务端和客户端同时支持长连接才可以进行通信
如果不清楚长连接和短连接,可以去看看我上篇文章,里面有介绍长连接和短连接,HTTP 和 HTTPS,有介绍 HTTP 的长短连接,实质上就是 TCP 长短连接
客户端:
public class Client { public static void main(String[] args) throws IOException { Scanner sc = new Scanner(System.in); Socket socket = new Socket("127.0.0.1", 8080); OutputStream os = socket.getOutputStream(); OutputStreamWriter writer = new OutputStreamWriter(os, StandardCharsets.UTF_8); PrintWriter printWriter = new PrintWriter(writer); InputStream is = socket.getInputStream(); Scanner scanner = new Scanner(is, "UTF-8"); while (sc.hasNextLine()) { String str = sc.nextLine(); printWriter.printf("我是请求:\r\n%s\r\n", str); printWriter.flush(); // 接收响应 String header = scanner.nextLine(); String response = scanner.nextLine(); Log.println(header + "\r\n" + response); } socket.close(); } }
服务端:
public class Server { private final static HashMap<String, String> map = new HashMap<>(); static { map.put("苹果", "apple"); map.put("香蕉", "banana"); map.put("梨", "pear"); map.put("电脑", "computer"); map.put("手机", "phone"); } private final static class ServerTask implements Runnable { private final Socket socket; private ServerTask(Socket socket) { this.socket = socket; } @Override public void run() { InetAddress address = socket.getInetAddress(); Log.println("客户端 IP 地址为:" + address); int port = socket.getPort(); Log.println("客户端端口号为:" + port); try { InputStream is = socket.getInputStream(); // 请求格式必须以 “我是请求:\r\n” 开始,以 “\r\n” 结束 Scanner sc = new Scanner(is, "UTF-8"); // 进行响应 OutputStream os = socket.getOutputStream(); OutputStreamWriter writer = new OutputStreamWriter(os, StandardCharsets.UTF_8); PrintWriter printWriter = new PrintWriter(writer); while (sc.hasNextLine()) { String header = sc.nextLine(); String EnglishWord = sc.nextLine(); Log.println("请求中的词为:" + EnglishWord); // 对请求进行处理 String ChineseWord = map.getOrDefault(EnglishWord, "我不知道"); Log.println("翻译后的词:" + ChineseWord); Log.println("准备发起响应"); printWriter.printf("OK!\r\n%s\r\n", ChineseWord); printWriter.flush(); Log.println("响应成功"); } socket.close(); Log.println("连接已关闭"); } catch (IOException e) { e.printStackTrace(); } } } public static void main(String[] args) throws IOException { // 使用多线程完成多用户同时在线的功能 ExecutorService threadPool = Executors.newFixedThreadPool(3); ServerSocket serverSocket = new ServerSocket(8080); Log.println("绑定端口到 8080"); while (true) { Log.println("等待客户端连接"); Socket socket = serverSocket.accept(); Log.println("客户端连接成功"); ServerTask serverTask = new ServerTask(socket); threadPool.execute(serverTask); } } }