上节回顾
socket(插槽,操作系统提供的网络编程的API的统称)
进行网络编程的核心就是通过代码操作网卡这个硬件设备
操作系统对于网卡进行了抽象,进程想去操作网卡的时候,就会打开一个"socket 文件"
通过读写这个socket文件,就能读写网卡了.
系统提供的socketAPI主要有两种风格:
1.基于UDP的(数据报)
2.基于TCP的(字节流)
UDP:
1.无连接
2.不可靠
3.面向数据报
4.全双工
TCP:
1.有连接
2.可靠数据流
3.面向字节流
4.全双工
在UDP socket中,主要提供了这么两个类:
DatagramSocket:对 socket 文件进行了封装.
构造方法:
1.无参:客户端使用,此时端口号由系统分配.
2.传入端口号:服务器使用,此时端口号是用户指定
receive()方法:读取一个数据,并且放到DatagramPacket中.(可能会阻塞)
send()方法:发送一个UDP数据报
DatagramPacket:对 一个 UDP 数据报进行了封装
构造方法:
1.传入空的缓冲区,构造一个空的packet(receive的时候使用的)
2.传入一个有数据的缓冲区,指定一下目标ip 和 端口
3.传入一个有数据的缓冲区,指定一下目标ip 和 端口(inetSockAddress类来完成)
2和3一般都是send的时候使用
一个服务器的核心流程:
1.读取请求并解析.
2.根据请求计算响应
3.把响应写回给客户端
一个客户端的核心流程:
1.根据用户输入,构造请求.
2.发送请求个服务器
3.读取服务器的响应
4.解析响应并显示
在这一系列流程中,哪个环节是最复杂,或者是消耗代码最多的呢?
根据请求计算响应
因为上一篇咱们写的是回显服务器,不涉及这里的逻辑.
但是其他一些有意义的服务器程序,这个环节往往就很复杂了.
这是服务器程序最核心的部分.
体现了具体的业务逻辑.
第二个版本的UDP程序.
不再是回显服务了,而是"翻译程序"
英译汉,客户端输入的请求是英文单词,返回的响应是对应的中文解释.
服务器代码
import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.SocketException; import java.util.HashMap; /** * Created with IntelliJ IDEA. * Description: * User: 灯泡和大白 * Date: 2022-07-25 * Time: 21:45 */ public class UdpDictServer { private DatagramSocket socket = null; private HashMap<String,String> dict = new HashMap<>(); public UdpDictServer(int port) throws SocketException { socket = new DatagramSocket(port); //对哈希表的值进行初始化 //此处这个表的数据可以非常多. dict.put("hello","你好"); dict.put("cat","猫咪"); dict.put("dog","小狗"); } private String process(String request) { // 根据请求计算响应 // 例如用户的请求是 "hello", 就应该返回一个 "你好" // 这个逻辑的实现, 核心就是 "查表" // 像有道这样的专业的词典, 应该是把数据都放到数据库中的. // 这里的查表就是查数据库的表了. // 当前咱们简单期间, 就直接查内存的 hash 表. 我们也完全可以去把这里的数据放到数据库中. // 此处使用 getOrDefault 来查. 如果是 get 的话, key 不存在, 就返回 null 了. // 此处期望返回的不是 null, 而是给客户端一个提示. return dict.getOrDefault(request, "[单词在词典中不存在!]"); } public void start() throws IOException { System.out.println("服务器启动!"); while (true) { // 1. 读取请求并解析 DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096); socket.receive(requestPacket); 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); String log = String.format("[%s : %d] req: %s; resp: %s",requestPacket.getAddress().toString(), requestPacket.getPort(),request,response); System.out.println(log); } } public static void main(String[] args) throws IOException { UdpDictServer sever = new UdpDictServer(9090); sever.start(); } }
客户端代码
import java.io.IOException; import java.net.*; import java.util.Scanner; /** * Created with IntelliJ IDEA. * Description: * User: 灯泡和大白 * Date: 2022-07-25 * Time: 22:34 */ //这个类和上一篇的UdpEchoClient基本类似 //主要是因为客户端要负责和用户交互.而当下这个程序和回显程序都是用户交互的方式差不多. //此时就不需要有太多的改变. public class UdpDictClient { private DatagramSocket socket = null; private String serverIP ; private int serverPort; public UdpDictClient(String serverIP, int serverPort) throws SocketException { this.serverIP = serverIP; this.serverPort = serverPort; this.socket = new DatagramSocket(); } public void start() throws IOException { Scanner scanner = new Scanner(System.in); while (true) { //1.读取输入的数据 System.out.println("->"); String request = scanner.next(); if (request.equals("exit")) { System.out.println("goodbye"); return; } //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); String response = new String(responsePacket.getData(), 0, responsePacket.getLength()); // 4. 把数据显示给用户 String log = String.format("req: %s; resp: %s", request, response); System.out.println(log); } } public static void main(String[] args) throws IOException { UdpDictClient client = new UdpDictClient("127.0.0.1", 9090); client.start(); } }
UDP暂告一段落
TCP流套接字API
相关的两个类:
ServerSocket类:
accept()方法 和 TCP "有连接" 这样的特性密切相关.
accpet就是接电话这个动作!
客户端尝试建立连接,首先是服务器操作系统这一层来和客户端进行一些相关的流程,把这个连接先准备好.
用户代码调用accept,才能真的把这个连接拿到用户代码中
socket也是对应到文件了.
也会有一个close方法.
一个socket理论上用完了之后是要关闭的.但是咱们前面写的UDP版本的程序其实不太需要关闭.(也不能关闭)
当前这里的UdpServer UdpClient 里面的 socket 是有生命周期的,都是要跟随整个程序的.
如果socket/文件没有关闭,当进程结束的时候,对应的资源也就自然释放了.
Socket类:
使用TCP写一个简单的程序
回显服务器,回显客户端
服务器代码
import sun.rmi.transport.tcp.TCPChannel; 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 listenSocket = null; public TcpEchoServer(int port) throws IOException { listenSocket = new ServerSocket(port); } public void start() throws IOException { System.out.println("服务器启动!"); while (true) { // UDP 的服务器进入主循环, 就直接尝试 receive 读取请求了. // 但是 TCP 是有连接的. 先需要做的是, 建立好连接 // 当服务器运行的时候, 当前是否有客户端来建立连接, 不确定~~ // 如果客户端没有建立连接, accept 就会阻塞等待 // 如果有客户端建立连接了, 此时 accept 就会返回一个 Socket 对象 // 进一步的服务器和客户端之间的交互, 就交给 clientSocket 来完成了~ Socket clientSocket = listenSocket.accept(); processConnection(clientSocket); } } private void processConnection(Socket clientSocket) throws IOException { // 处理一个连接. 在这个连接中可能会涉及客户端和服务器之间的多次交互 String log = String.format("[%s:%d] 客户端上线!", clientSocket.getInetAddress().toString(), clientSocket.getPort()); System.out.println(log); try (InputStream inputStream = clientSocket.getInputStream(); OutputStream outputStream = clientSocket.getOutputStream()) { while (true) { // 1. 读取请求并解析 // 可以直接通过 inputStream 的 read 把数据读到一个 byte[] , 然后再转成一个 String // 但是比较麻烦. 还可以借助 Scanner 来完成这个工作. Scanner scanner = new Scanner(inputStream); if (!scanner.hasNext()) { log = String.format("[%s:%d] 客户端下线!", clientSocket.getInetAddress().toString(), clientSocket.getPort()); System.out.println(); break; } String request = scanner.next(); // 2. 根据请求计算响应 String response = process(request); // 3. 把响应写回给客户端 PrintWriter writer = new PrintWriter(outputStream); writer.println(response); writer.flush(); log = String.format("[%s:%d] req: %s; resp: %s", clientSocket.getInetAddress().toString(), clientSocket.getPort(), request, response); System.out.println(log); } } catch (IOException e) { e.printStackTrace(); } finally { // 当前的 clientSocket 生命周期, 不是跟随整个程序, 而是和连接相关. // 因此就需要每个连接结束, 都要进行关闭. // 否则随着连接的增多, 这个 socket 文件就可能出现资源泄露的情况 clientSocket.close(); } } // 当前是实现一个回显服务器 // 客户端发啥, 服务器就返回啥. private String process(String request) { return request; } public static void main(String[] args) throws IOException { TcpEchoServer server = new TcpEchoServer(9090); server.start(); } }
服务器代码
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 String serverIp; private int serverPort; private Socket socket = null; public TcpEchoClient(String serverIp, int serverPort) throws IOException { this.serverIp = serverIp; this.serverPort = serverPort; // 让 socket 创建的同时, 就和服务器尝试建立连接 this.socket = new Socket(serverIp, serverPort); } public void start() { 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")) { break; } // 2. 把这个读取的内容构造成请求, 发送给服务器 PrintWriter printWriter = new PrintWriter(outputStream); printWriter.println(request); // println 只是把数据写到缓冲区里, 至于是不是真的写到 IO 设备, 不好说. // 加上一个 flush 强制发送一下~ printWriter.flush(); // 3. 从服务器读取响应并解析 Scanner respScanner = new Scanner(inputStream); String response = respScanner.next(); // 4. 把结果显示到界面上. String log = String.format("req: %s; resp: %s", request, response); System.out.println(log); } } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) throws IOException { TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090); client.start(); } }
![]()