服务器与客户端交互小栗子(java代码实现最基本的服务器实例)

简介: 服务器与客户端交互小栗子(java代码实现最基本的服务器实例)

一、一些相关的概念解释

我们知道在网络编程中经常会涉及到各种各样的概念,我们来重温一下这些概念

c444ab7aa92e4331a231e4de793b8dc7.png

在进行网络编程中,我们经常会用到操作系统给我们提供的网络编程API(这些api也叫socket  api),这些原生的API大多是c语言,于是在我们java中,JVM非常贴心的把这些c风格的 socket api封装了一下,变成了我们Java中面向对象风格的api


  • 在我们网络的TCP/IP五层(或四层)模型中正是在其中的传输层,提供了网络通信api —— socket api
  • 传输层提供了两个非常重要的协议,按照不同的协议TCP和UDP,这两个协议所对应的socket api也是截然不同的


🍑UDP协议所对应的api

6fa04b2cb94244bfab0e34354e999b3c.png

二、实例分析

1、UDP

🌰 UDP版本的 回显服务器-客户端

接下来我们通过一个实例来演示这些api的具体用法

比如我们现在要写一个UDP版本的 回显服务器 + 客户端的这样一个交互程序(客户端发什么,服务器就返回什么)

对于UDP协议来说,具有无连接,面向数据报的特征,即每次都是没有建立连接,并且一次发送全部数据报,一次接收全部的数据报。

java中使用UDP协议通信,主要基于 DatagramSocket 类来创建数据报套接字,并使用

DatagramPacket 作为发送或接收的UDP数据报

8f8f7f02d7844e9a818dc6c6d9266fa7.png

结合上图我们也可以分析出来该实现服务器和客户端之间回显操作的基本流程


0d90ecc6941d47249a460fe196499ab3.png

服务器代码:

package network;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
// 回显服务器
public class UdpEchoServer {
    // 要创建一个服务器,先打开一个socket文件
    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为基本单位
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(requestPacket); // 把接收到的数据包放到该方法的参数中(输出型参数)如果此时客户端没有发送请求,就该方法就陷入阻塞等待中
            // 2、对请求进行解析,把DatagramPacket转成一个String
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
            // 3.处理请求
            String response = process(request);
            // 4、把响应构造成DatagramPacket对象
            // 构造响应对象,要搞清楚,对象要发给谁,谁给咱们发的请求,咱就发给谁,response.length返回的是字符个数,response.getBytes().length返回的是字节个数
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length, requestPacket.getSocketAddress());
            // 5、发送响应给客户
            socket.send(responsePacket);
            // 打印日志, requestPacket.getAddress()返回读取的客户端IP地址, requestPacket.getPort()返回读取的客户端端口号
            System.out.printf("发送方的ip和端口号:[%s:%d] request:%s, response:%s", requestPacket.getAddress().toString(), requestPacket.getPort(), request, response);
            System.out.println();
        }
    }
    // 对请求进行响应处理
    public String process(String request) {
        return request; // 回显处理就是请求发的是什么就响应什么
    }
    public static void main(String[] args) throws IOException {
        UdpEchoServer udpEchoServer = new UdpEchoServer(8000);
        udpEchoServer.start();
    }
}

客户端代码

package network;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
public class UdpEchoClient {
    private DatagramSocket socket = null;
    public UdpEchoClient() throws SocketException {
        socket = new DatagramSocket(); // 在客户端,我们一般不自己指定端口号,而是由系统分配
    }
    public void start() throws IOException {
        while (true) {
            System.out.println("当前是客户端!");
            System.out.print(">");
            // 1、客户端从键盘输入请求的具体内容
            Scanner scanner = new Scanner(System.in);
            String request = scanner.nextLine();
            // 要说明要给谁发送请求,他的ip地址和端口号是多少(服务器的)
            // DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.length(), InetAddress.getByName("127.0.0.1"), 8000);
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
                    InetAddress.getByName("127.0.0.1"), 8000);
            // 2、客户端向服务器发送请求
            socket.send(requestPacket);
            // 3、客户端接收服务器返回的响应
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(responsePacket); // 注意我们是把服务器的响应存放到了responsePacket中,不要写错
            // 4、客户端显示服务器返回的响应信息,进行解析转换成String类型的信息
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
            System.out.printf("发送方的ip地址和端口号:[%s:%s],  request: %s, response: %s\n", requestPacket.getAddress(), requestPacket.getPort(), request, response);
        }
    }
    public static void main(String[] args) throws IOException {
        UdpEchoClient client = new UdpEchoClient();
        client.start();
    }
}

一个服务器可以为多个客户端提供服务

fee7cc8a2ee4e633b236ee320e9b50f2.png

🌰UDP版本的 字典服务器-客户端

不同服务器他们的读取请求并解析、构造响应并返回的整体大逻辑是一样的,所不同就就在于对请求的处理,一个服务器要完成的工作,都是通过”根据请求计算响应“来体现的,这个步骤也是服务器的”灵魂所在“

  • 我们的字典服务器完全可以在上述回显服务器的基础上完成工作——只需要改变那个根据请求来计算响应的步骤
  • 至于客户端,其实和上述回显服务器的客户端是一样的——都是发不同的请求而已,连改都不用改


服务器代码:

package network;
import java.io.IOException;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
// 字典服务器
public class UdpDicServer extends UdpEchoServer {
    public Map<String, String> map = new HashMap<>();
    public UdpDicServer(int port) throws SocketException { // 构造方法
        super(port);
        map.put("synchronized", "同步的");
        map.put("volatile", "易变的");
    }
    @Override
    public String process(String request) {
        return map.getOrDefault(request, "抱歉,当前尚未收录此词!");
    }
    public static void main(String[] args) throws IOException {
        UdpDicServer server = new UdpDicServer(8000);
        server.start();
    }
}

客户端代码:

package network;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
public class UdpEchoClient {
    private DatagramSocket socket = null;
    public UdpEchoClient() throws SocketException {
        socket = new DatagramSocket(); // 在客户端,我们一般不自己指定端口号,而是由系统分配
    }
    public void start() throws IOException {
        while (true) {
            System.out.println("当前是客户端!");
            System.out.print(">");
            // 1、客户端从键盘输入请求的具体内容
            Scanner scanner = new Scanner(System.in);
            String request = scanner.nextLine();
            // 要说明要给谁发送请求,他的ip地址和端口号是多少(服务器的)
            // DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.length(), InetAddress.getByName("127.0.0.1"), 8000);
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
                    InetAddress.getByName("127.0.0.1"), 8000);
            // 2、客户端向服务器发送请求
            socket.send(requestPacket);
            // 3、客户端接收服务器返回的响应
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(responsePacket); // 注意我们是把服务器的响应存放到了responsePacket中,不要写错
            // 4、客户端显示服务器返回的响应信息,进行解析转换成String类型的信息
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
            System.out.printf("发送方的ip地址和端口号:[%s:%s],  request: %s, response: %s\n", requestPacket.getAddress(), requestPacket.getPort(), request, response);
        }
    }
    public static void main(String[] args) throws IOException {
        UdpEchoClient client = new UdpEchoClient();
        client.start();
    }
}

客户端测试:

5fa6423a11774c29866d62ee62433b04.png

17e55702888047dfafeef08f25b1c4ec.png

服务器测试:

0ca9dbbdb076419199dabf16aeac5f5a.png

2、TCP


454f7a9bfb8d4635b4ada7ba4a1e4a3c.png


50ef5a60b93249318fc8f923da51affc.png

🌰 TCP版本的 回显服务器-客户端 (多线程+线程池版本)

服务器代码

package network;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TcpEchoServer {
    ServerSocket serverSocket = null;
    public TcpEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }
    public void start() throws IOException {
        ExecutorService service = Executors.newCachedThreadPool();// 此处不太适合使用 "固定个数的"
        while (true) {
            // 服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket
            // 如果当前没有客户端来建立连接, 就会阻塞等待!!
            Socket clientSocket = serverSocket.accept();
            // processConnection(clientSocket); // 单线程模式——即该服务器无法同时为多个客户端同时提供服务
            // [版本2] 多线程版本. 主线程负责拉客, 新线程负责通信
            // 虽然比版本1 有提升了, 但是涉及到频繁创建销毁线程, 在高并发的情况下, 负担比较重的.
//            Thread thread = new Thread(() -> {
//                try {
//                    processConnection(clientSocket);
//                } catch (IOException e) {
//                    e.printStackTrace();
//                }
//            });
//            thread.start();
            // [版本3] 使用线程池, 来解决频繁创建销毁线程的问题.
            // 此处不太适合使用 "固定个数的"
            service.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        processConnection(clientSocket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
    // 通过这个方法, 给当前连上的这个客户端, 提供服务!!
    // 这里我们建立长连接——一次连接中,会有 N次请求,N次响应
    public void processConnection(Socket clientSocket) throws IOException {
        System.out.printf("连接成功![%s:%d]\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream() ) {
            Scanner scannerNet = new Scanner(inputStream); // 对我们的读取进行嵌套
            PrintWriter printWriter = new PrintWriter(outputStream);
            while (true) {
                if (!scannerNet.hasNext()) {
                    // 说明此时连接中断,服务器无法继续从客户端读取到请求
                    // 连接断开. 当客户端断开连接的时候, 此时 hasNext 就会返回 false
                    System.out.printf("[%s:%d] 断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
                    break;
                }
                // 1、读取客户端的请求
                String request = scannerNet.nextLine();
                // 从客户端读取到的请求,如果客户端在输入请求的时候,只是通过nextLine敲了回车,然后就通过printWrite.write发送给我们的服务器,
                // 这时服务器接收到的数据是不带换行符的,换行符在我们的客户端nextLine输入的过程中就已经被吞吃了,所有为了避免String request = scanner.nextLine();无法正常读取(nextLine只有遇到换行才结束)
                // 所以在我们的客户端在发送时要通过PrintWrite.println()来发送,会给我们自动添加一个换行符
                // 2、处理客户端发来的请求
                String response = process(request);
                // 3、把响应发送给客户端
                printWriter.println(response);
                // printWriter.write(response); // 这样写有bug,因为我们当前的response中没有带换行符,
                printWriter.flush(); // 刷新缓冲区
                // 打印日志
                System.out.printf("[%s:%d]  request: %s, response: %s\n",
                        clientSocket.getInetAddress(), clientSocket.getPort(),
                        request, response);
            }
        }
        finally {
            clientSocket.close(); // 每一次建立连接都会创建一个clientSocket,该连接结束后要及时关闭,避免内存泄漏
        }
    }
    public String process(String request) {
        return request;
    }
    public static void main(String[] args) throws IOException {
        TcpEchoServer server = new TcpEchoServer(8000);
        server.start();
    }
}

客户端代码

package network;
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 {
    Socket socket = null;
    public TcpEchoClient() throws IOException {
        // 指定客户端要连接的服务器
        socket = new Socket("127.0.0.1", 8000);
        // 程序走到了这里,说明客户端和服务器就已经成功建立了连接
    }
    public void start() throws IOException {
        Scanner scanner = new Scanner(System.in);
        System.out.println("当前是客户端,请输入请求");
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()) { // InputStream用于读取, OutputStream用于发送
            // 对我们的读取和发送进行写包装,使得读取和包装更方便
            Scanner scannerNet = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);
            // 这里我们建立长连接——一次连接中,会有 N次请求,N次响应
            while (true) {
                System.out.print("> ");
                // 1、客户端从键盘输入请求
                String request = scanner.nextLine();
                // 在这里我们虽然在输入内容后用换行符来进行了结束,但我们用于接收的request并没有读取的换行符——它被nextLine()给吞吃了
                // 2、把请求发送给服务器
                printWriter.println(request);
                // printWriter.write(request);
                // 这么写会产生一个bug, 要printWriter.println()会自动给我们要写入的添加一个换行符
                // 可以让服务器在读取请求时遇到换行就结束,不至于陷入阻塞
                printWriter.flush(); // 刷新缓冲区
                // 3、从服务器读取响应内容
                String response = scannerNet.nextLine();
                // 4、打印日志
                System.out.printf("request: %s, response: %s\n", request, response);
            }
        }
        finally {
            socket.close();
        }
}
    public static void main(String[] args) throws IOException {
        TcpEchoClient client = new TcpEchoClient();
        client.start();
    }
}

测试

d2a23fdbeb4845aabbabfc5c30e261db.png


相关文章
|
5月前
|
Shell 网络安全 开发工具
服务器已经搭建好的项目如何关联至gitee对应仓库并且将服务器的项目代码推送至gitee-优雅草卓伊凡
服务器已经搭建好的项目如何关联至gitee对应仓库并且将服务器的项目代码推送至gitee-优雅草卓伊凡
366 5
|
10月前
|
人工智能 Java API
MCP客户端调用看这一篇就够了(Java版)
本文详细介绍了MCP(Model Context Protocol)客户端的开发方法,包括在没有MCP时的痛点、MCP的作用以及如何通过Spring-AI框架和原生SDK调用MCP服务。文章首先分析了MCP协议的必要性,接着分别讲解了Spring-AI框架和自研SDK的使用方式,涵盖配置LLM接口、工具注入、动态封装工具等步骤,并提供了代码示例。此外,还记录了开发过程中遇到的问题及解决办法,如版本冲突、服务连接超时等。最后,文章探讨了框架与原生SDK的选择,认为框架适合快速构建应用,而原生SDK更适合平台级开发,强调了两者结合使用的价值。
13212 33
MCP客户端调用看这一篇就够了(Java版)
|
6月前
|
存储 Java 关系型数据库
Java 项目实战基于面向对象思想的汽车租赁系统开发实例 汽车租赁系统 Java 面向对象项目实战
本文介绍基于Java面向对象编程的汽车租赁系统技术方案与应用实例,涵盖系统功能需求分析、类设计、数据库设计及具体代码实现,帮助开发者掌握Java在实际项目中的应用。
266 0
|
9月前
|
人工智能 Java 开发者
【Java实例-简易计算机】使用Java实现简单的计算机案例
一个简单的Java案例——“简易计算器”,帮助编程新手快速上手。通过实现用户输入、基本逻辑运算和结果输出,学习者可以掌握变量声明、Scanner对象使用、控制流语句等关键知识点。文章分为设计思路、关键知识点、完整代码和测试运行四个部分。
268 9
【Java实例-简易计算机】使用Java实现简单的计算机案例
|
8月前
|
安全 Java 测试技术
Java 大学期末实操项目在线图书管理系统开发实例及关键技术解析实操项目
本项目基于Spring Boot 3.0与Java 17,实现在线图书管理系统,涵盖CRUD操作、RESTful API、安全认证及单元测试,助力学生掌握现代Java开发核心技能。
458 0
|
Java Linux 定位技术
Minecraft配置文件参数说明(JAVA服务器篇)
Minecraft JAVA版服务器启动后会生成server.properties配置文件,位于minecraft_server/根目录下。该文件包含多项关键设置,如游戏模式(gamemode)、最大玩家数(max-players)、难度(difficulty)等。此文档详细说明了各配置项的功能与默认值,帮助用户高效管理服务器环境。
3243 60
|
10月前
|
存储 网络协议 Java
Java获取客户端IP问题:返回127.0.0.1
总结:要解决Java获取客户端IP返回127.0.0.1的问题,首先要找出原因,再采取合适的解决方案。请参考上述方案来改进代码,确保在各种网络环境下都能正确获取客户端IP地址。希望本文对您有所帮助。
658 25
|
10月前
|
Java 测试技术 项目管理
【JavaEE】从 0 到 1 掌握 Maven 构建 Java 项目核心技巧 解锁 Java 项目高效管理实用实例
本文从Maven基础概念讲起,涵盖安装配置、核心概念(如POM与依赖管理)及优化技巧。结合Java Web项目实例,演示如何用Maven构建和管理项目,解决常见问题,助你高效掌握这一强大工具,提升Java开发与项目管理能力。适合初学者及进阶开发者学习。资源链接:[点此获取](https://pan.quark.cn/s/14fcf913bae6)。
344 6
|
9月前
|
Java 开发者
【Java实例-神秘年龄】用Java挑战你的直觉
我们一起走进这款款简单却充满趣味的Java小游戏——“神秘年龄”。这款游戏不仅适合编程初学者作为练习项目,也能为有一定基础的开发者提供一个轻松的编程小憩。
117 0
【Java实例-神秘年龄】用Java挑战你的直觉
|
9月前
|
Java 开发者
【Java实例-神秘硬币】用Java投掷你的幸运硬币,你是猜正还是反?
本文分享了一个简单有趣的编程案例——猜硬币正反面游戏。通过模拟抛硬币(0为正面,1为反面),用户输入猜测值,程序判断结果并输出。
221 0
【Java实例-神秘硬币】用Java投掷你的幸运硬币,你是猜正还是反?