前提概括
首先进入本章知识重点之前,我们要先了解清楚计算机在客户端以及服务端扮演的角色以及交流过程.
接收端和发送端
在⼀次网络数据传输时:
发送端:数据的发送方进程,称为发送端。发送端主机即网络通信中的源主机。
接收端:数据的接收方进程,称为接收端。接收端主机即网络通信中的目的主机。
注意:发送端和接收端只是相对的,只是⼀次网络数据传输产生数据流向后的概念。
客户端和服务端
服务端:在常见的网络数据传输场景下,把提供服务的一方进程,称为服务端,可以提供对外服务。
客户端:获取服务的一方进程,称为客户端。
客户端和服务端交流过程
最常见的场景,客户端是指给用户使用的程序,服务端是提供用户服务的程序:
- 客户端先发送请求到服务端
- 服务端根据请求数据,执行相应的业务处理
- 服务端返回响应:发送业务处理结果
- 客户端根据响应数据,展示处理结果(展示获取的资源,或提示保存资源的处理结果).
一: TCP协议
传输层TCP协议
TCP,即Transmission Control Protocol(传输控制协议),传输层协议
1.1:TCP协议的六大特性
- 有连接
- 双全工
- 可靠传输
- 面向字节流
- 有接收缓冲区,也有发送缓冲区
- 大小不限
对于字节流来说,可以简单的理解为,传输数据是基于IO流,流式数据的特征就是在IO流没有关闭的
情况下,是无边界的数据,可以多次发送,也可以分开多次接收
1.2:Socket
Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服
务端Socket。
不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据
的。
⽅法签名 | ⽅法说明 |
Socket(String host,int port) | 创建⼀个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接 |
Socket 方法:
⽅法签名 | ⽅法说明 |
InetAddress getInetAddress() | 返回套接字所连接的地址 |
InputStream getInputStream() | 返回此套接字的输入流 |
OutputStream getOutputStream() | 返回此套接字的输出流 |
1.3:ServerSocket
ServerSocket 是创建TCP服务端Socket的API。
ServerSocket 构造方法:
⽅法签名 | ⽅法说明 |
ServerSocket(int port) | 创建⼀个服务端流套接字Socket,并绑定到指定端口 |
ServerSocket 方法:
⽅法签名 | ⽅法说明 |
Socket accept() | 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回⼀个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待 |
void close() | 关闭此套接字 |
1.4:TCP的实现
- 客户端和服务端:开发时,经常是基于⼀个主机开启两个进程作为客户端和服务端,但真实的场
景,⼀般都是不同主机。 - 注意目的IP和目的端口号,标识了⼀次数据传输时要发送数据的终点主机和进程
- Socket编程我们是使用流套接字和数据报套接字,基于传输层的TCP或UDP协议,但应用层协议,
也需要考虑,这块我们在后续来说明如何设计应用层协议。 - 关于端口被占用的问题
- 如果⼀个进程A已经绑定了⼀个端口,再启动⼀个进程B绑定该端口,就会报错,这种情况也叫端口
被占用。对于java进程来说,端口被占用的常见报错信息如下:
TCP Echo Server服务器
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 = null; // 这个操作就会绑定端⼝号 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().t // 接下来就可以读取请求, 根据请求计算响应, 返回响应三步⾛了. // Socket 对象内部包含了两个字节流对象, 可以把这俩字节流对象获取到, 完成后续的读写 try (InputStream inputStream = clientSocket.getInputStream(); OutputStream outputStream = clientSocket.getOutputStream()) { // ⼀次连接中, 可能会涉及到多次请求/响应 while (true) { // 1. 读取请求并解析. 为了读取⽅便, 直接使⽤ Scanner. Scanner scanner = new Scanner(inputStream); if (!scanner.hasNext()) { // 读取完毕, 客⼾端下线. System.out.printf("[%s:%d] 客⼾端下线!\n", clientSocket.getIne break; } // 这个代码暗含⼀个约定, 客⼾端发过来的请求, 得是⽂本数据, 同时, 还得带有 String request = scanner.next(); // 2. 根据请求计算响应 String response = process(request); // 3. 把响应写回给客⼾端. 把 OutputStream 使⽤ PrinterWriter 包裹⼀下 PrintWriter writer = new PrintWriter(outputStream); // 使⽤ PrintWriter 的 println ⽅法, 把响应返回给客⼾端. // 此处⽤ println, ⽽不是 print 就是为了在结尾加上 \n . ⽅便客⼾端 writer.println(response); // 这⾥还需要加⼀个 "刷新缓冲区" 操作. writer.flush(); // ⽇志, 打印当前的请求详情. System.out.printf("[%s:%d] req: %s, resp: %s\n", clientSocket.ge request, response); } } catch (IOException e) { e.printStackTrace(); } finally { // 在 finally 中加上 close 操作, 确保当前 socket 被及时关闭!! try { clientSocket.close(); } catch (IOException e) { e.printStackTrace(); } } } 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客服端
import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.net.Socket; public class TcpEchoClient { private Socket socket = null; // 要和服务器通信, 就需要先知道, 服务器所在的位置. public TcpEchoClient(String serverIp, int serverPort) throws IOException { // 这个 new 操作完成之后, 就完成了 tcp 连接的建⽴. socket = new Socket(serverIp, serverPort); } public void start() { System.out.println("客⼾端启动"); Scanner scannerConsole = new Scanner(System.in); try (InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()) { while (true) { // 1. 从控制台输⼊字符串. System.out.print("-> "); String request = scannerConsole.next(); // 2. 把请求发送给服务器 PrintWriter printWriter = new PrintWriter(outputStream); // 使⽤ println 带上换⾏. 后续服务器读取请求, 就可以使⽤ scanner.n printWriter.println(request); // 不要忘记 flush, 确保数据是真的发送出去了!! printWriter.flush(); // 3. 从服务器读取响应. Scanner scannerNetwork = new Scanner(inputStream); String response = scannerNetwork.next(); // 4. 把响应打印出来 System.out.println(response); } } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) throws IOException { TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090); client.start(); } }
二: UDP协议
传输层UDP协议
UDP,即User Datagram Protocol(用户数据报协议),传输层协议。
2.1:UDP协议的六大特特性
- 无连接
- 双全工
- 不可靠传输
- 面向数据报
- 有接收缓冲区,无发送缓冲区
- 大小受限:⼀次最多传输64k
对于数据报来说,可以简单的理解为,传输数据是⼀块⼀块的,发送⼀块数据假如100个字节,必须⼀
次发送,接收也必须⼀次接收100个字节,而不能分100次,每次接收1个字节。
2.2: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() | 关闭此数据报套接字 |
2.3:DatagramPacket
DatagramPacket是UDP Socket发送和接收的数据报。
DatagramPacket 构造方法:
⽅法签名 | ⽅法说明 |
DatagramPacket(byte[]buf,int length) | 构造⼀个DatagramPacket以⽤来接收数据报,接收的数据保存在字节数组(第⼀个参数buf)中,接收指定⻓度(第⼆个参数length) |
DatagramPacket(byte[] buf,int offset,int length,SocketAddress address) | 构造⼀个DatagramPacket以用来发送数据报,发送的数据为字节数组(第⼀个参数buf)中,从0到指定长度(第⼆个参数length)。address指定⽬的主机的IP和端⼝号 |
DatagramPacket 方法:
⽅法签名 | ⽅法说明 |
InetAddress getAddress() | 从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址 |
int getPort() | 从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端⼝号 |
byte[] getData() | 获取数据报中的数据构造UDP发送的数据报时 |
2.4:UDP的实现
对于UDP协议来说,具有无连接,面向数据报的特征,即每次都是没有建立连接,并且⼀次发送全部
数据报,⼀次接收全部的数据报。
java中使用UDP协议通信,主要基于 DatagramSocket 类来创建数据报套接字,并使用
DatagramPacket 作为发送或接收的UDP数据报。对于⼀次发送及接收UDP数据报的流程如下:
UDP Echo Server服务器
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], 40 socket.receive(requestPacket); // 这样的转字符串的前提是, 后续客⼾端发的数据就是⼀个⽂本的字符串. String request = new String(requestPacket.getData(), 0, requestPacke // 2. 根据请求, 计算出响应 String response = process(request); // 3. 把响应写回给客⼾端 // 此时需要告知⽹卡, 要发的内容是啥, 要发给谁. DatagramPacket responsePacket = new DatagramPacket(response.getBytes requestPacket.getSocketAddress()); socket.send(responsePacket); // 记录⽇志, ⽅便观察程序执⾏效果. System.out.printf("[%s:%d] req: %s, resp: %s\n", requestPacket.getAd 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客服端
import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.SocketException; import java.util.Scanner; public class UdpEchoClient { private DatagramSocket socket = null; private String serverIp; private int serverPort; // 服务器的 ip 和 服务器的端⼝. public UdpEchoClient(String ip, int port) throws SocketException { serverIp = ip; serverPort = port; // 这个 new 操作, 就不再指定端⼝了. 让系统⾃动分配⼀个空闲端⼝. socket = new DatagramSocket(); } // 让这个客⼾端反复的从控制台读取⽤⼾输⼊的内容. 把这个内容构造成 UDP 请求, 发给服务器 // 最终再显⽰在客⼾端的屏幕上. public void start() throws IOException { Scanner scanner = new Scanner(System.in); System.out.println("客⼾端启动!"); while (true) { // 1. 从控制台读取⽤⼾输⼊的内容 System.out.print("-> "); // 命令提⽰符, 提⽰⽤⼾要输⼊字符串. String request = scanner.next(); // 2. 构造请求对象, 并发给服务器. DatagramPacket requestPacket = new DatagramPacket(request.getBytes() InetAddress.getByName(serverIp), serverPort); socket.send(requestPacket); // 3. 读取服务器的响应, 并解析出响应内容. DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4 socket.receive(responsePacket); String response = new String(responsePacket.getData(), 0, responsePa // 4. 显⽰到屏幕上. System.out.println(response); } } public static void main(String[] args) throws IOException { UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090); // UdpEchoClient client = new UdpEchoClient("42.192.83.143", 9090); client.start(); } }
如果觉得文章不错,期待你的一键三连哦,你个鼓励是我创作的动力之源,让我们一起加油,顶峰相见!!!💓 💓 💓