JavaEE-网络编程套接字(UDP/TCP)







下面写一个简单的UDP客户端服务器流程

思路:

对于服务器端:读取请求,并解析--> 根据解析出的请求,做出响应(这里是一个回显,)-->把响应写回客户端

对于客户端:从控制台读取用户输入的内容-->从控制台读取用户输入的内容-->从控制台读取用户输入的内容-->将其显示在屏幕上

全部代码如下:

服务器端:

java 复制代码
package network;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
//UDP的回显服务器   客户端发出的请求是啥,服务器返回的响应就是啥
public class UdpEchoServer {
    private DatagramSocket socket=null;

    //  指定服务器的port
    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 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);
            //记录日志
            System.out.printf("[%s:%d]  req: %s, resp: %s\n",requestPacket.getAddress().toString(),requestPacket.getPort(),
                    request,response);
        }
    }
    //这里是一个回显,只需要返回这个字符串
    public String process(String request){
        return request;
    }

    public static void main(String[] args) throws IOException {
        UdpEchoServer udpEchoServer=new UdpEchoServer(9090);
        udpEchoServer.start();
    }
}

客户端:

java 复制代码
package network;

import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
    //由于客户端的port是自动分配的,所以这里不会像服务器那样配置port
    //但是,客户端需要向服务器发送请求,所以,这里我们需要知道服务器的ip和port
    private DatagramSocket socket=null;
    private String serverIp;
    private int serverPort;

    //外部指定服务器的ip和port
    public UdpEchoClient(String ip,int port) throws SocketException {
        this.serverIp=ip;
        this.serverPort=port;
        //客户端的port是自动分配的
        socket=new DatagramSocket();

    }
    // 让这个客户端反复的从控制台读取用户输入的内容. 把这个内容构造成 UDP 请求, 发给服务器. 再读取服务器返回的 UDP 响应
    // 最终再显示在客户端的屏幕上.
    public void  start() throws IOException {
        Scanner scanner=new Scanner(System.in);
        System.out.println("客户端开始启动");
        while(true){
            //1. 从控制台读取用户输入的内容
            System.out.println("->");
            String requset=scanner.next();
            //2.构造请求对象,发送给服务器
            DatagramPacket requsetPacket=new DatagramPacket(requset.getBytes(),requset.getBytes().length,
                    InetAddress.getByName(serverIp),serverPort);
            socket.send(requsetPacket);
            //3.读取服务器的响应,并解析出其内容
            DatagramPacket responsePacket=new DatagramPacket(new byte[4096],4096);
            socket.receive(responsePacket);
            String response=new String(responsePacket.getData(),0,responsePacket.getLength());
            //4 。将其显示在屏幕上
            System.out.println(response);
        }
    }
    public static void main(String[] args) throws IOException {
        UdpEchoClient udpEchoClient=new UdpEchoClient("127.0.0.1",9090);//127.0.0.1 本机ip
        udpEchoClient.start();

    }
}

运行结果如下

对上述过程中的一些谈论和分析:



多个客户端向一个服务器发送请求



下面写一个简单的翻译服务器

重写的服务器端的代码如下:

java 复制代码
package network;

import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;

public class UdpDictServer extends UdpEchoServer{
    //使用HashMap保存中英文翻译的键值对
    private Map<String,String> dict =new HashMap<>();
    //实现父类的构造方法
    public UdpDictServer(int port) throws SocketException {
        super(port);

        //一些原始的键值对
        dict.put("cat","猫");
        dict.put("dog","狗");
        dict.put("people","人");
    }
    //与原始的UdpEachServer相比,这里对于请求的处理过程是不一样的
    //重写process方法

    @Override
    public String process(String request) {
        //找到对应的翻译,并返回
        //getOrDefault方法,找到key所对应的value值,如果没有找到,则返回defaultValue(即第二个参数)
        return dict.getOrDefault(request,"该词没有查询到");
    }

    public static void main(String[] args) throws IOException {
        UdpDictServer server=new UdpDictServer(9090);
        // start 不需要重新再写一遍了. 直接就复用了之前的 start
        server.start();
    }
}

执行结果如下:




下面写一个基于TCP 的回显流程

思路:

服务器端:先从队列中拿到一个"连接"--> 读取请求并解析-->根据请求计算响应-->把响应写回给客户端

客户端:从控制台输入字符串-->把请求发送给服务器-->从服务器读取响应.-->把响应打印出来

全部代码如下:

服务器端代码:

java 复制代码
package network;

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;

//基于TCP的回显服务器
public class TcpEachServer {
    private ServerSocket serverSocket=null;

    //绑定端口号
    public  TcpEachServer(int port) throws IOException {
        serverSocket=new ServerSocket(port);
    }

    //启动服务器
    public  void  start() throws IOException {
        System.out.println("服务器开始启动");
        while(true){
            //从管理连接的队列中拿出一个"连接"出来
            Socket clientSocket=serverSocket.accept();
            //处理这个连接内的请求
            processConnection(clientSocket);
        }
    }
    //这个方法用来处理连接中的逻辑
    private void processConnection(Socket clientSocket) throws IOException {
        //日志
        System.out.printf("[%s:%d] 客户端上线\n",clientSocket.getInetAddress().toString(),
                clientSocket.getPort());
        //下面开始读取请求,计算响应,返回响应  三步曲
        //Socket对象内部包含两种字节流对象InputStream和OutputStream,可以先把这两个对象流获
        // 取到,方便后续处理过程种的读写工作
        try(InputStream inputStream=clientSocket.getInputStream();
            OutputStream outputStream=clientSocket.getOutputStream()){
            //不同于UDP协议中的无连接,在客户端的一次连接过程中,可能涉及多次请求/响应过程
            //因此。这里使用一个while循环,直到该连接中的所有请求处理完毕
            while(true){
                //1,读取请求并解析
                Scanner scanner=new Scanner(inputStream);
                //hasNext的作用是,检测输入流中是否有结束输入的控制符,比如0x1A(EOF,Ctrl-Z)
                //用于检测一个连接是否结束
                if(!scanner.hasNext()){
                    //一个连接处理完毕
                    System.out.printf("[%s:%d] 客户端本次连接处理完毕,下线!\n",clientSocket.getInetAddress().toString(),
                            clientSocket.getPort());
                    break;
                }
                // 这个代码暗含一个约定, 客户端发过来的请求, 得是文本数据, 同时, 还得带有空白符作为分割. (比如换行这种)
                //next():当输入到空白符结束
                String request=scanner.next();
                //2.根据请求计算响应
               String response=process(request);
               //3. 把响应写回客户端,把OutputStream用PrintWriter(此处的PrintWriter相当于Scanner)包裹一下,便于发送数据
                //将outputStream和PrintWriter关联起来
                PrintWriter writer=new PrintWriter(outputStream);
                //使用 PrintWriter 的 println 方法,打印到输出流中 把响应返回给客户端.
                //此处用 println, 而不是 print 就是为了在结尾加上 \n . 方便客户端读取响应, 使用 scanner.next 读取.
                writer.println(response);
                //这里还需要加一个 "刷新缓冲区" 操作.将缓冲区的数据强制输出,用于清空缓冲区
                writer.flush();

                //日志 记录当前的请求和响应
                System.out.printf("[%s:%d]  req: %s,resp: %s\n",clientSocket.getInetAddress().toString(),
                        clientSocket.getPort(),request,response);
            }
        }
    }

    //回显,只需要再返回这个字符串
    public String process(String requset){
        return requset;
    }
    public static void main(String[] args) throws IOException {
        TcpEachServer tcpEachServer=new TcpEachServer(9090);
        tcpEachServer.start();
    }
}

客户端代码:

java 复制代码
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 {
    private Socket socket=null;

    //服务器端的ip和port
    public  TcpEchoClient(String serverIp,int serverPort) throws IOException {
        //这个new的动作完成后,完成了tcp的建立
        socket=new Socket(serverIp,serverPort);
    }

    public void start() throws IOException {
        System.out.println("客户端启动");
        Scanner scannerConsole=new Scanner(System.in);
        //Socket对象内部包含两种字节流对象InputStream和OutputStream,可以先把这两个对象流获
        //   取到,方便后续处理过程种的读写工作
        try(InputStream inputStream=socket.getInputStream();
            OutputStream outputStream=socket.getOutputStream()){
            while(true){
                //1.从控制台输入字符串
                System.out.println("-->");
                String request=scannerConsole.next();
                //2.把请求发送给服务器  需要对request进行包装,使用PrintWriter
                PrintWriter printWriter=new PrintWriter(outputStream);
                //使用 println 带上换行. 后续服务器读取请求, 就可以使用 scanner.next 来获取了
                printWriter.println(request);//发送请求
                printWriter.flush();
                //3.从服务器中接收响应
                Scanner scannerNetwork=new Scanner(inputStream);
                String response=scannerNetwork.next();
                //4.把响应打印出来
                System.out.println(response);
            }
        }
    }
    public static void main(String[] args) throws IOException {
        TcpEchoClient tcpEchoClient=new TcpEchoClient("127.0.0.1",9090);
        tcpEchoClient.start();
    }
}

当开多个线程时,发现只有一个线程在被处理,其它线程都在等待,

当被处理的线程下线后,其他线程的逻辑才开始被处理

原因在于 Socket clientSocket = serverSocket.accept();和processConnection(clientSocket);都是主线程进行处理的且在同一次循环体中,只有一个clinetSocket连接被处理完后,才会去队列中accept下一个连接,为此,这里我们可以采用多线程进行处理。

修改为多线程后,可以看到 有多个客户端可以访问服务器

考虑到一个现实的情况,许多客户端需要频繁的访问服务器,那就是需要频繁的断开/连接,我们这里可以使用线程池

同样也可以实现多个客户端同时访问服务器。

最终的服务器的代码如下:

java 复制代码
package network;

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;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
//基于TCP的回显服务器
public class TcpEachServer {
    private ServerSocket serverSocket=null;
    //创建一个非固定数目的线程池
    private ExecutorService service= Executors.newCachedThreadPool();

    //绑定端口号
    public  TcpEachServer(int port) throws IOException {
        serverSocket=new ServerSocket(port);
    }

    //启动服务器
    public  void  start() throws IOException {
        System.out.println("服务器开始启动");
        while(true){
            //从管理连接的队列中拿出一个"连接"出来
            Socket clientSocket=serverSocket.accept();
            //处理这个连接内的请求
            service.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        processConnection(clientSocket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
/*            Thread t=new Thread(() ->{
                try {
                    processConnection(clientSocket);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
            t.start();*/
        }
    }
    //这个方法用来处理连接中的逻辑
    private void processConnection(Socket clientSocket) throws IOException {
        //日志
        System.out.printf("[%s:%d] 客户端上线\n",clientSocket.getInetAddress().toString(),
                clientSocket.getPort());
        //下面开始读取请求,计算响应,返回响应  三步曲
        //Socket对象内部包含两种字节流对象InputStream和OutputStream,可以先把这两个对象流获
        // 取到,方便后续处理过程种的读写工作
        try(InputStream inputStream=clientSocket.getInputStream();
            OutputStream outputStream=clientSocket.getOutputStream()){
            //不同于UDP协议中的无连接,在客户端的一次连接过程中,可能涉及多次请求/响应过程
            //因此。这里使用一个while循环,直到该连接中的所有请求处理完毕
            while(true){
                //1,读取请求并解析
                Scanner scanner=new Scanner(inputStream);
                //hasNext的作用是,检测输入流中是否有结束输入的控制符,比如0x1A(EOF,Ctrl-Z)
                //用于检测一个连接是否结束
                if(!scanner.hasNext()){
                    //一个连接处理完毕
                    System.out.printf("[%s:%d] 客户端本次连接处理完毕,下线!\n",clientSocket.getInetAddress().toString(),
                            clientSocket.getPort());
                    break;
                }
                // 这个代码暗含一个约定, 客户端发过来的请求, 得是文本数据, 同时, 还得带有空白符作为分割. (比如换行这种)
                //next():当输入到空白符结束
                String request=scanner.next();
                //2.根据请求计算响应
               String response=process(request);
               //3. 把响应写回客户端,把OutputStream用PrintWriter(此处的PrintWriter相当于Scanner)包裹一下,便于发送数据
                //将outputStream和PrintWriter关联起来
                PrintWriter writer=new PrintWriter(outputStream);
                //使用 PrintWriter 的 println 方法,打印到输出流中 把响应返回给客户端.
                //此处用 println, 而不是 print 就是为了在结尾加上 \n . 方便客户端读取响应, 使用 scanner.next 读取.
                writer.println(response);
                //这里还需要加一个 "刷新缓冲区" 操作.将缓冲区的数据强制输出,用于清空缓冲区
                writer.flush();

                //日志 记录当前的请求和响应
                System.out.printf("[%s:%d]  req: %s,resp: %s\n",clientSocket.getInetAddress().toString(),
                        clientSocket.getPort(),request,response);
            }
        } finally {
            clientSocket.close();
        }
    }

    //回显,只需要再返回这个字符串
    public String process(String requset){
        return requset;
    }
    public static void main(String[] args) throws IOException {
        TcpEachServer tcpEachServer=new TcpEachServer(9090);
        tcpEachServer.start();
    }
}

上述过程中的一些思路



相关推荐
Cry丶4 小时前
ubuntu安装vsftpd
服务器·网络·ubuntu
SHENGWEI胜为7 小时前
交换机端口显示正常,终端却获取不到 IP 地址?从 DHCP 流程定位故障
服务器·网络·测试工具·缓存·通信工程
IMPYLH9 小时前
Linux 系统备份与镜像
linux·服务器·网络
淘气包海鸟10 小时前
水声网络框架
网络·ns3·水
国科安芯11 小时前
星间激光通信终端CANFD控制网络的时延确定性与辐射可靠性
网络·嵌入式硬件·信息与通信·canfd·低轨卫星·商业航天·抗辐射
我命由我1234511 小时前
Python 开发 - Python 虚拟环境
运维·开发语言·后端·python·http·https·java-ee
中屹指纹浏览器svip11 小时前
网络抖动会标记账号异常?分清两种波动
网络
承渊政道11 小时前
Linux网络学习【传输层协议UDP】
linux·网络·网络协议·学习·ubuntu·udp
小此方12 小时前
Linux网络(三十三):告别O(n)噩梦:从select/poll的挣扎到epoll红黑树+回调的降维打击,一文打通IO多路复用
linux·网络·驱动开发
2301_7779983412 小时前
Linux 网络:数据链路层
linux·服务器·网络