【网络编程】理解客户端和服务器并使用Java提供的api实现回显服务器

2024-03-05 1145阅读

温馨提示:这篇文章已超过384天没有更新,请注意相关的内容是否还可用!

目录

一、网络编程

二、客户端和服务器

三、客户端和服务器的交互模式

四、TCP 和 UDP

UDP socket api 的使用

1、DatagramSoket

2、DatagramPacket

TCP socket api 的使用

1、ServerSocket

2、Socket 



【网络编程】理解客户端和服务器并使用Java提供的api实现回显服务器

一、网络编程

本质上就是学习传输层给应用层提供的 api,通过 api 把数据交给传输层,进一步地层层封装将数据通过网卡发送出去,这也是网络程序的基本工作流程。

掌握了基础 api 就能更好的理解实际开发中使用的框架(spring,dubbo)的工作过程,也提供了魔改/自己实现框架的能力。

二、客户端和服务器

在网络中,主动发起通信的一方称为“客户端”,被动接受的一方称为“服务器”。同一个程序在不同的场景中,可能是客户端也可能是服务器。

客户端给服务器发送的数据,称为“请求”(request);

服务器给客户端返回的数据,称为“响应”(response);

【网络编程】理解客户端和服务器并使用Java提供的api实现回显服务器

三、客户端和服务器的交互模式

1、“一问一答”

一个请求对于一个响应,这是交互模式是最常见的,后续进行的“网站开发”(web开发)都是这种模式。

2、“一问多答”

主要在“下载”场景中涉及

3、“多问一答”

主要在“上传”场景中涉及

4、“多问多答”

主要在“远程控制/远程桌面”场景中涉及

四、TCP 和 UDP

进行网络编程,需要使用系统的 API,【本质上是传输层提供的协议】。

传输层主要涉及到两个协议:TCP 和 UDP。

连接性可靠性面向数据传输方式
TCP面向连接可靠传输面向字节流全双工
UDP无连接不可靠传输面向数据报全双工
  • 连接:此处说的“连接”不是物理意义的连接,是抽象虚拟的“连接”。所谓计算机中的“网络连接”是指通信双方各自保存对方的信息。客户端的数据结构中记录了谁是它的服务器;服务器的数据结构中记录了谁是它的客户端;本质上就是记录对方的信息。
  • 可靠传输/不可靠传输:无论如何都不能保证100%的信息传输。可靠传输主要是指发送方能够感知数据有没有传输给接收方,如果没接收到,可以采取相应的措施补救,例如重传机制。
  • 面向字节流:与文件中的字节流完全一致,网络中传输数据的基本单位就是字节。
  • 面向数据报:每次传输的基本单位是一个数据报(有一系列字节构成)。
  • 全双工:一个信道,可以双向通信,就叫全双工。可以理解成马路的多车道,就是全双工。
  • 半双工:可以理解为吸管,同一时刻只能吸或者呼。

    UDP socket api 的使用

    Java 把系统原生 api 封装了,UDP socket 提供的两个核心的类:

    1、DatagramSoket

    操作系统中有一类文件,就叫 socket 文件,这类文件抽象地表示了“网卡”这样的硬件设备。而进行网络通信最核心的硬件设备就是网卡。

    DatagramSocket 类就是负责对 socket 文件进行读写,从而借助网卡发送接收数据。

    【网络编程】理解客户端和服务器并使用Java提供的api实现回显服务器

    2、DatagramPacket

    UDP 面向数据报,每次发送接收数据的基本单位是一个 UDP 数据报。

    DatagramPacket 类就表示了一个 UDP 数据报。

    【网络编程】理解客户端和服务器并使用Java提供的api实现回显服务器 关于 receive 接收数据报的底层实现过程

    UdpEchoServer 实例

    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], 4096);
                socket.receive(requestPacket);
                // 将读到的字节数组转换成 String 方便后续操作
                String request = new String(requestPacket.getData(),0,requestPacket.getLength());
                //2. 根据请求计算响应
                String response = process(request);
                //3. 把响应返回到客户端
                // 与请求数据报创建不同,请求数据报是使用空白字节数组,而此处直接把 String 里包含的字节数组作为参数创建,
                DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
                        requestPacket.getSocketAddress());  // 因为 UDP 无连接,因此必须从【请求数据报】中获取对应客户端的 ip 和端口
                socket.send(responsePacket);
                //打印日志
                System.out.printf("[%s:%d] req: %s, resp: %s\n",requestPacket.getAddress().toString(),
                        requestPacket.getPort(), request, response);
            }
        }
        private String process(String request) {
            return request;
        }
        public static void main(String[] args) throws IOException {
            UdpEchoServer server = new UdpEchoServer(9090);
            server.start();
        }
    }

     上述代码中:

    1、可以看到 23 行需要从【请求数据报】中获取对应客户端 ip 和端口号才能完成发送响应,证明了 UDP socket 自身不保存对端的 ip 和端口号,体现了无连接。

    2、不可靠传输,代码中没有体现。

    3、receive 和 socket 都是以DatagramPacket 为单位,体现了面向数据报。

    4、一个 socket 既能发送(send)有能接收(receive),体现了全双工。

    UdpEchoClient 示例 

    public class UdpEchoClient {
        private DatagramSocket socket = null;
        private String serverIp;
        private int serverPort;
        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 sc = new Scanner(System.in);
            while (true) {
                //1. 从控制台读取要发送的数据
                System.out.print("-> "); //表示提示用户输入
                if (!sc.hasNext()) {   //hasNext 具有阻塞功能
                    break;
                }
                String request = sc.next();
                //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);
                //4. 把响应显示到控制台
                String response = new String(responsePacket.getData(),0,responsePacket.getLength());
                System.out.println(response);
            }
        }
        public static void main(String[] args) throws IOException {
            UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);
            client.start();
        }
    }

    TCP socket api 的使用

    由于 TCP 是面向字节流的,传输的基本单位是字节,因此没有像 UDP 中 DatagramPacket 这样的类。

    Java 把系统原生 api 封装了,TCP socket 提供的两个核心的类:

    1、ServerSocket

    这是Socket 类,同样抽象地表示了“网卡”但是这个类与 UDP 中使用的 DatagramSocket 不同,这个类只能给服务器进行使用。只负责处理对客户端的连接,主要 api 是 accept()。

    2、Socket 

    对应到“网卡”,既能给服务器使用,又能给客户端使用。相当于电话的两个听筒,通过 Socket 完成对端之间的通信。主要的 api 是 getInputStream 和 getOutputStream。
    需要注意:由于服务器端的 Socket 对象与客户端时一一对应的,为了避免无限占用文件描述符表,使用完毕后需要 close 关闭。

    TcpEchoServer 示例

    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 后(new socket),就会和对应的服务器进行 tcp 连接建立流程
                // 此时通过 accept 方法来“接听电话”,然后才能进行通信
                Socket clientSocket = serverSocket.accept();
                Thread t = new Thread(() -> {
                    processConnection(clientSocket);
                });
                t.start();
            }
        }
        private void processConnection(Socket clientSocket) {
            System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
            // 循环读取客户端的请求并返回响应
            try (InputStream inputStream = clientSocket.getInputStream();
                 OutputStream outputStream = clientSocket.getOutputStream()){
                // 可以使用 inputStream 原本的 read 方法进行读取
                // 但是比较繁琐,为了【方便读入】,这里使用 Scanner 对输入流进行输入
                Scanner sc = new Scanner(inputStream);
                while (true) {   // 长连接
                    if (!sc.hasNext()) {
                        // 读取完毕,客户端断开连接
                        System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
                        break;
                    }
                    //1. 读取请求并解析,此处使用 next ,需要注意 next 的读入规则
                    String request = sc.next();
                    //2. 根据请求计算响应
                    String response = process(request);
                    //3. 把响应返回给客户端
                    /*  通过这种方式也可以写回,但是这种方式不方便添加 \n
                    outputStream.write(response.getBytes(),0,response.getBytes().length);*/
                    // 因此为了【方便写入】,给 outputStream 也套一层,即使用 printWriter
                    // 此处的 printWriter 就类似于 Scanner 将输入流包装了一下,而 printWriter 对输出流包装了一下
                    PrintWriter printWriter = new PrintWriter(outputStream);
                    // 通过 println 在末尾添加了 \n,与客户端的 scNetwork.next 呼应
                    printWriter.println(response);
                    // 刷新缓冲区,确保数据能够发送出去
                    printWriter.flush();
                    // 打印日志
                    System.out.printf("[%s:%d] req: %s, resp: %s\n", clientSocket.getInetAddress(), clientSocket.getPort(),
                            request,response);
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            } finally {
                    clientSocket.close();
            }
        } 
        private String process(String request) {
            return request;
        }
        public static void main(String[] args) throws IOException {
            TcpEchoServer server = new TcpEchoServer(9090);
            server.start();
        }
    }

    需要注意的是:

    1、理解ServerSocket 和Socket 的不同作用,Socket作为接收对象。

    2、只有当客户端 new Socket 时,ServerSocket 才能通过 accept 完成连接。

    3、Scanner 和 PrintWriter 。

    4、flush 刷新缓冲区。

    5、finaly{ clientSocket.close(); } 每个客户端对应一个Socket,因此每个客户端完成任务后,需要关闭文件,从而销毁文件描述符表。而 try()自动关闭的是流对象,而没有释放文件本体。

    TcpEchoClient 示例

    public class TcpEchoClient {
        private Socket socket = null;
        public TcpEchoClient(String serverIp, int serverPort) throws IOException {
            // 这里直接将 ip 和 port 传入,是由于 tcp 是有连接的,socket 里能够保存 ip 和 port
            socket = new Socket(serverIp,serverPort);
            // 因此也不需要额外创建【类成员对象】来保存 ip 和 port
        }
        public void start() {
            System.out.println("客户端启动!");
            try (InputStream inputStream = socket.getInputStream();
                 OutputStream outputStream = socket.getOutputStream()){
                // 此处的 scanner 用于控制台读取数据
                Scanner scConsole = new Scanner(System.in);
                // 此处的 scanner 用于读取服务器响应回来的数据
                Scanner scNetwork = new Scanner(inputStream);
                // 此处 printWriter 用于向服务器写入请求数据
                PrintWriter printWriter = new PrintWriter(outputStream);
                while (true) {
                    // 这里流程和 UDP 的客户端类似
                    //1. 从控制台读取输入的字符串
                    System.out.print("-> ");
                    if (!scConsole.hasNext()) {
                        break;
                    }
                    String request = scConsole.next();
                    //2. 把请求发送给服务器,
                    // 使用 printWriter 是为了使发送的请求末尾带有 \n,与服务器的 sc.next 呼应
                    printWriter.println(request);
                    // 刷新缓冲区,确保数据能够发送出去
                    printWriter.flush();
                    //3. 从服务器读取响应
                    String response = scNetwork.next();
                    //4. 打印响应
                    System.out.println(response);
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        public static void main(String[] args) throws IOException {
            TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
            client.start();
        }
    }

    需要注意的是:

    1、TCP 是有连接的,因此 Socket 能够直接保存 ip 和 port。

    2、flush 刷新缓冲区。

    【网络编程】理解客户端和服务器并使用Java提供的api实现回显服务器

    【博主推荐】

    【Java多线程】面试常考——锁策略、synchronized的锁升级优化过程以及CAS(Compare and swap)-CSDN博客【网络编程】理解客户端和服务器并使用Java提供的api实现回显服务器https://blog.csdn.net/zzzzzhxxx/article/details/136288256?spm=1001.2014.3001.5501【Java多线程】对线程池的理解并模拟实现线程池-CSDN博客【网络编程】理解客户端和服务器并使用Java提供的api实现回显服务器https://blog.csdn.net/zzzzzhxxx/article/details/136160003?spm=1001.2014.3001.5501【Java多线程】分析线程加锁导致的死锁问题以及解决方案-CSDN博客【网络编程】理解客户端和服务器并使用Java提供的api实现回显服务器https://blog.csdn.net/zzzzzhxxx/article/details/136150237?spm=1001.2014.3001.5501

    如果觉得作者写的不错,求给博主一个大大的点赞支持一下,你们的支持是我更新的最大动力!

    如果觉得作者写的不错,求给博主一个大大的点赞支持一下,你们的支持是我更新的最大动力!

    如果觉得作者写的不错,求给博主一个大大的点赞支持一下,你们的支持是我更新的最大动力!

VPS购买请点击我

免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们,邮箱:ciyunidc@ciyunshuju.com。本站只作为美观性配图使用,无任何非法侵犯第三方意图,一切解释权归图片著作权方,本站不承担任何责任。如有恶意碰瓷者,必当奉陪到底严惩不贷!

目录[+]