【Java成王之路】EE初阶第九篇:(网络编程) 3

发布于:2022-07-26 ⋅ 阅读:(276) ⋅ 点赞:(0)

上节回顾

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();
    }
}

 

 


网站公告

今日签到

点亮在社区的每一天
去签到