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

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

一、网络编程

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


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


二、客户端和服务器

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


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


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



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

1、“一问一答”


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


2、“一问多答”


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


3、“多问一答”


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


4、“多问多答”


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


四、TCP 和 UDP

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


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


image.png

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

UDP socket api 的使用

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


1、DatagramSoket

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


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



2、DatagramPacket

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


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


关于 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 刷新缓冲区。

目录
相关文章
|
26天前
|
JSON 中间件 Go
Go 网络编程:HTTP服务与客户端开发
Go 语言的 `net/http` 包功能强大,可快速构建高并发 HTTP 服务。本文从创建简单 HTTP 服务入手,逐步讲解请求与响应对象、URL 参数处理、自定义路由、JSON 接口、静态文件服务、中间件编写及 HTTPS 配置等内容。通过示例代码展示如何使用 `http.HandleFunc`、`http.ServeMux`、`http.Client` 等工具实现常见功能,帮助开发者掌握构建高效 Web 应用的核心技能。
160 61
|
2月前
|
人工智能 Java API
MCP客户端调用看这一篇就够了(Java版)
本文详细介绍了MCP(Model Context Protocol)客户端的开发方法,包括在没有MCP时的痛点、MCP的作用以及如何通过Spring-AI框架和原生SDK调用MCP服务。文章首先分析了MCP协议的必要性,接着分别讲解了Spring-AI框架和自研SDK的使用方式,涵盖配置LLM接口、工具注入、动态封装工具等步骤,并提供了代码示例。此外,还记录了开发过程中遇到的问题及解决办法,如版本冲突、服务连接超时等。最后,文章探讨了框架与原生SDK的选择,认为框架适合快速构建应用,而原生SDK更适合平台级开发,强调了两者结合使用的价值。
2903 31
MCP客户端调用看这一篇就够了(Java版)
|
27天前
|
运维 网络协议 Go
Go网络编程:基于TCP的网络服务端与客户端
本文介绍了使用 Go 语言的 `net` 包开发 TCP 网络服务的基础与进阶内容。首先简述了 TCP 协议的基本概念和通信流程,接着详细讲解了服务端与客户端的开发步骤,并提供了简单回显服务的示例代码。同时,文章探讨了服务端并发处理连接的方法,以及粘包/拆包、异常检测、超时控制等进阶技巧。最后通过群聊服务端的实战案例巩固知识点,并总结了 TCP 在高可靠性场景中的优势及 Go 并发模型带来的便利性。
|
2月前
|
存储 网络协议 Java
Java获取客户端IP问题:返回127.0.0.1
总结:要解决Java获取客户端IP返回127.0.0.1的问题,首先要找出原因,再采取合适的解决方案。请参考上述方案来改进代码,确保在各种网络环境下都能正确获取客户端IP地址。希望本文对您有所帮助。
190 25
|
4月前
|
前端开发 Cloud Native Java
Java||Springboot读取本地目录的文件和文件结构,读取服务器文档目录数据供前端渲染的API实现
博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
Java||Springboot读取本地目录的文件和文件结构,读取服务器文档目录数据供前端渲染的API实现
|
4月前
|
Kubernetes API 网络安全
当node节点kubectl 命令无法连接到 Kubernetes API 服务器
当Node节点上的 `kubectl`无法连接到Kubernetes API服务器时,可以通过以上步骤逐步排查和解决问题。首先确保网络连接正常,验证 `kubeconfig`文件配置正确,检查API服务器和Node节点的状态,最后排除防火墙或网络策略的干扰,并通过重启服务恢复正常连接。通过这些措施,可以有效解决与Kubernetes API服务器通信的常见问题,从而保障集群的正常运行。
288 17
|
4月前
|
存储 网络协议 安全
Java网络编程,多线程,IO流综合小项目一一ChatBoxes
**项目介绍**:本项目实现了一个基于TCP协议的C/S架构控制台聊天室,支持局域网内多客户端同时聊天。用户需注册并登录,用户名唯一,密码格式为字母开头加纯数字。登录后可实时聊天,服务端负责验证用户信息并转发消息。 **项目亮点**: - **C/S架构**:客户端与服务端通过TCP连接通信。 - **多线程**:采用多线程处理多个客户端的并发请求,确保实时交互。 - **IO流**:使用BufferedReader和BufferedWriter进行数据传输,确保高效稳定的通信。 - **线程安全**:通过同步代码块和锁机制保证共享数据的安全性。
170 23
|
4月前
|
缓存 安全 Java
《从头开始学java,一天一个知识点》之:字符串处理:String类的核心API
🌱 **《字符串处理:String类的核心API》一分钟速通!** 本文快速介绍Java中String类的3个高频API:`substring`、`indexOf`和`split`,并通过代码示例展示其用法。重点提示:`substring`的结束索引不包含该位置,`split`支持正则表达式。进一步探讨了String不可变性的高效设计原理及企业级编码规范,如避免使用`new String()`、拼接时使用`StringBuilder`等。最后通过互动解密游戏帮助读者巩固知识。 (上一篇:《多维数组与常见操作》 | 下一篇预告:《输入与输出:Scanner与System类》)
116 11
|
5月前
|
数据采集 存储 Java
Java爬虫获取微店店铺所有商品API接口设计与实现
本文介绍如何使用Java设计并实现一个爬虫程序,以获取微店店铺的所有商品信息。通过HttpClient发送HTTP请求,Jsoup解析HTML页面,提取商品名称、价格、图片链接等数据,并将其存储到本地文件或数据库中。文中详细描述了爬虫的设计思路、代码实现及注意事项,包括反爬虫机制、数据合法性和性能优化。此方法可帮助商家了解竞争对手,为消费者提供更全面的商品比较。
|
5月前
|
安全 网络协议 Java
Java网络编程封装
Java网络编程封装原理旨在隐藏底层通信细节,提供简洁、安全的高层接口。通过简化开发、提高安全性和增强可维护性,封装使开发者能更高效地进行网络应用开发。常见的封装层次包括套接字层(如Socket和ServerSocket类),以及更高层次的HTTP请求封装(如RestTemplate)。示例代码展示了如何使用RestTemplate简化HTTP请求的发送与处理,确保代码清晰易维护。

热门文章

最新文章