目录
♫什么是网络编程
♫Socket套接字
♪什么是Socket套接字
♪数据报套接字
♪流套接字
♫数据报套接字通信模型
♪数据报套接字通讯模型
♪DatagramSocket
♪DatagramPacket
♪实现UDP的服务端代码
♪实现UDP的客户端代码
♫流套接字通信模型
♪流套接字通讯模型
♪ServerSocket
♪Socket
♪实现TCP的服务端代码
♪实现TCP的客户端代码
网络编程,指网络上的主机,通过不同的进程,以编程的方式实现网络数据传输。在网络编程中,获取一个网络资源,涉及到两次网络数据传输: 发送端请求数据的发送 和 接受端响应数据的发送,其中提供服务的一端叫做接受端,获取服务的一端叫做客户端。
♪什么是Socket套接字
Socket套接字是由操作系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元,是对TCP/IP协议栈的封装(尤其是对传输层的TCP和UDP协议的封装),所以当应用程序使用Socket进行通信时,实际上是在使用TCP或UDP协议进行数据传输。基于 Socket 套接字的网络程序开发就是网络编程。
Socket套接字主要针对传输层协议划分为流套接字和数据报套接字。
♪数据报套接字
数据报套接字是使用传输层 UDP,即 User Datagram Protocol(用户数据报协议),传输层协议。 以下为UDP的特点:
♩.无连接:通信双方在发送数据之前不需要建立连接
♩.不可靠传输:不保证数据包的可靠传输,即数据包可能会丢失、重复或乱序到达接收方
♩.面向数据报:UDP将应用层的数据作为独立的报文进行处理,每个报文都被封装在一个UDP数据报中
♩.全双工:通信双方都可以互相进行通信
♩.大小受限:一次最多传输64k
♪流套接字
流套接字是使用传输层 TCP,即 Transmission Control Protocol(传输控制协议),传输层协议。 以下为TCP的特点:
♩.有连接:服务端需和客户端建立连接后才可以进行通信
♩.可靠传输:确认应答和超时重传共同保证
♩.面向字节流:对于字节流来说,可以简单的理解为,传输数据是基于IO流,流式数据的特征就是在IO流没有关闭的情况下,是无边界的数据,可以多次发送,也可以分开多次接收
♩.全双工:通信双方都可以互相进行通信
♪数据报套接字通讯模型
对于 UDP 协议来说,具有无连接,面向数据报的特征,即每次都是没有建立连接,并且一次发送全部数据报,一次接收全部的数据报。 java中使用 UDP 协议通信,主要基于 DatagramSocket 类来创建数据报套接字,并使用 DatagramPacket 作为发送或接收的 UDP 数据报。对于发送及接收UDP数据报的流程如下:
♪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() 关闭此数据报套接字
♪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的服务端代码
import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.SocketException; // UDP 版本的回显服务器 class UdpEchoServer { // 网络编程, 本质上是要操作网卡. // 但是网卡不方便直接操作. 在操作系统内核中, 使用了一种特殊的叫做 "socket" 这样的文件来抽象表示网卡. // 因此进行网络通信, 势必需要先有一个 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); socket.receive(requestPacket); // 此时这个 DatagramPacket 是一个特殊的对象, 并不方便直接进行处理. 可以把这里包含的数据拿出来, 构造成一个字符串. String request = new String(requestPacket.getData(), 0, requestPacket.getLength()); // 2. 根据请求计算响应, 由于此处是回显服务器, 响应和请求相同. String response = process(request); // 3. 把响应写回到客户端. send 的参数也是 DatagramPacket. 需要把这个 Packet 对象构造好. // 此处构造的响应对象, 不能是用空的字节数组构造了, 而是要使用响应数据来构造. DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length, requestPacket.getSocketAddress()); socket.send(responsePacket); // 4. 打印一下, 当前这次请求响应的处理中间结果. 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(); } }
♪实现UDP的客户端代码
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; // 一次通信, 需要有两个 ip, 两个端口. // 客户端的 ip 是 127.0.0.1 已知. // 客户端的 port 是系统自动分配的. // 服务器 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); // UdpEchoClient client = new UdpEchoClient("42.192.83.143", 9090); client.start(); } }
当我们分别启动客户端和服务器代码后,就可以进行本机上 UDP 版本的网络通信了:
注:若想开启多个客户端可以通过 Alt+U 修改配置使 idea 允许创建多个实例
♪流套接字通讯模型
对于 UDP 协议来说,具有有连接,面向字节流的特征,即每次都是需要建立连接,并且以流的方式发送数据。 java中使用 TCP 协议通信,主要基于 ServerSocket 类来接收连接,通过 Socket 类使用流的方式来收发数据。对于发送及接收 TCP 字节流的流程如下:
♪ServerSocket
ServerSocket 是创建TCP服务端Socket的API,可以接受来自客户端的连接请求。
♩.ServerSocket的构造方法
♩.ServerSocket的方法
构造方法 描述 ServerSocket(int port) 创建一个服务端流套接字Socket,并绑定到指定端口
方法 描述 Socket accept() 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket 对象,并基于该Socket建立与客户端的连接,否则阻塞等待 void close() 关闭此套接字 ♪Socket
Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端 Socket。 不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。
构造方法 描述 ServerSocket(int port) 创建一个服务端流套接字Socket,并绑定到指定端口 ♪实现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 = null; public TcpEchoServer(int port) throws IOException { serverSocket = new ServerSocket(port); } public void start() throws IOException { System.out.println("启动服务器"); while (true) { // 使用这个 clientSocket 和具体的客户端进行交流. Socket clientSocket = serverSocket.accept(); processConnection(clientSocket); } } // 使用这个方法来处理一个连接. // 这一个连接对应到一个客户端. 但是这里可能会涉及到多次交互. private void processConnection(Socket clientSocket) { 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.hasNext()) { // 没有下个数据, 说明读完了. (客户端关闭了连接) 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(response); // 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 { // 更合适的做法, 是把 close 放到 finally 里面, 保证一定能够执行到!! 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的客户端代码
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 { // 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); // 此处加上 flush 保证数据确实发送出去了. printWriter.flush(); // 3. 读取服务器的响应 Scanner respScanner = new Scanner(inputStream); String response = respScanner.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(); } }
分别运行客户端和服务器代码后就可以进行本机上 TCP 版本的网络通信了:
但是我们上面写的服务端代码如果与一个客户端建立连接后进入 processConnection 方法的 while 循环中,只要该客户端不下线,我们的就无法通过 accept 方法与第二个客户端建立连接,即当前服务器一次只能和一个客户端进行网络通信。要想让服务器能够同时和多个客户端进行通信,就需要引入多线程:
让服务器每建立一个连接就创建一个新线程与之通信,这里直接使用线程池的方式,避免线程频繁的创建销毁造成额外开销:
public void start() throws IOException { System.out.println("服务器启动!"); ExecutorService threadPool = Executors.newCachedThreadPool(); while (true) { Socket clientSocket = serverSocket.accept(); threadPool.submit(() -> { processCoonection(clientSocket); }); } }
这样子服务器就能同时和多个客户端进行 TCP 通信啦~