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

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 服务器与客户端交互小栗子(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


相关文章
|
28天前
|
Java
java小工具util系列5:java文件相关操作工具,包括读取服务器路径下文件,删除文件及子文件,删除文件夹等方法
java小工具util系列5:java文件相关操作工具,包括读取服务器路径下文件,删除文件及子文件,删除文件夹等方法
66 9
|
1月前
|
存储 Java API
Java实现导出多个excel表打包到zip文件中,供客户端另存为窗口下载
Java实现导出多个excel表打包到zip文件中,供客户端另存为窗口下载
44 4
|
2月前
|
Java
通过Java代码解释成员变量(实例变量)和局部变量的区别
本文通过一个Java示例,详细解释了成员变量(实例变量)和局部变量的区别。成员变量属于类的一部分,每个对象有独立的副本;局部变量则在方法或代码块内部声明,作用范围仅限于此。示例代码展示了如何在类中声明和使用这两种变量。
|
2月前
|
IDE 网络安全 开发工具
IDE之vscode:连接远程服务器代码(亲测OK),与pycharm链接服务器做对比(亲自使用过了),打开文件夹后切换文件夹。
本文介绍了如何使用VS Code通过Remote-SSH插件连接远程服务器进行代码开发,并与PyCharm进行了对比。作者认为VS Code在连接和配置多个服务器时更为简单,推荐使用VS Code。文章详细说明了VS Code的安装、远程插件安装、SSH配置文件编写、服务器连接以及如何在连接后切换文件夹。此外,还提供了使用密钥进行免密登录的方法和解决权限问题的步骤。
790 0
IDE之vscode:连接远程服务器代码(亲测OK),与pycharm链接服务器做对比(亲自使用过了),打开文件夹后切换文件夹。
|
2月前
|
运维 Java Linux
【运维基础知识】Linux服务器下手写启停Java程序脚本start.sh stop.sh及详细说明
### 启动Java程序脚本 `start.sh` 此脚本用于启动一个Java程序,设置JVM字符集为GBK,最大堆内存为3000M,并将程序的日志输出到`output.log`文件中,同时在后台运行。 ### 停止Java程序脚本 `stop.sh` 此脚本用于停止指定名称的服务(如`QuoteServer`),通过查找并终止该服务的Java进程,输出操作结果以确认是否成功。
61 1
|
2月前
|
IDE 网络安全 开发工具
IDE之pycharm:专业版本连接远程服务器代码,并配置远程python环境解释器(亲测OK)。
本文介绍了如何在PyCharm专业版中连接远程服务器并配置远程Python环境解释器,以便在服务器上运行代码。
414 0
IDE之pycharm:专业版本连接远程服务器代码,并配置远程python环境解释器(亲测OK)。
|
2月前
|
分布式计算 资源调度 Hadoop
大数据-01-基础环境搭建 超详细 Hadoop Java 环境变量 3节点云服务器 2C4G XML 集群配置 HDFS Yarn MapRedece
大数据-01-基础环境搭建 超详细 Hadoop Java 环境变量 3节点云服务器 2C4G XML 集群配置 HDFS Yarn MapRedece
85 4
|
2月前
|
Java Shell Maven
Flink-11 Flink Java 3分钟上手 打包Flink 提交任务至服务器执行 JobSubmit Maven打包Ja配置 maven-shade-plugin
Flink-11 Flink Java 3分钟上手 打包Flink 提交任务至服务器执行 JobSubmit Maven打包Ja配置 maven-shade-plugin
126 4
|
2月前
|
分布式计算 Java Hadoop
Hadoop-30 ZooKeeper集群 JavaAPI 客户端 POM Java操作ZK 监听节点 监听数据变化 创建节点 删除节点
Hadoop-30 ZooKeeper集群 JavaAPI 客户端 POM Java操作ZK 监听节点 监听数据变化 创建节点 删除节点
68 1
|
前端开发 Java Linux
Java服务器宕机解决方法论(上)
Java服务器宕机解决方法论(上)
755 0
Java服务器宕机解决方法论(上)