【JavaEE】socket网络编程的UDP实例与TCP实例

93 阅读11分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第18天,点击查看活动详情

文章目录

一、UDP回显服务

  所谓回显服务,英文是EchoServer,Echo的意思是回声,也就是说请求的内容是什么,得到的响应就是什么。这样的程序属于最简单的网络编程中的程序,不涉及到任何的业务逻辑,只是单纯的通过socket API转发。


在这里我们首先创建两个类,一个是UdpEchoServer类,另一个是UdpEchoClient。

1.UdpEchoServer

  针对UdpEchoServer:

(1)第一步是准备好socket文件,这是进行网络编程的大前提。

private DatagramSocket socket = null;

public UdpEchoServer(int port) throws SocketException {
        socket = new DatagramSocket(port);
    }

在这里插入图片描述

(2)启动服务器
由于我们这里是一个UDP服务器,而UDP不需要建立连接,直接接受从客服端来的数据即可。流程如下:

  1. 读取客户端发来的请求
  2. 根据请求计算响应,这里由于是一个回显服务,第二步就省略。
  3. 把响应写回客户端

  有的同学可能会疑惑,为啥这里服务器上来就是接收数据,而不是发送数据呢?

  被动接收请求的一方是服务器;主动发送请求的这一方叫做客户端。

第一步:读取客户端发来的请求:

//1.读取客户端发来的请求
      DatagramPacket requestPacket = new DatagramPacket(new byte[1024],1024);//需要指定里面的内存空间
      socket.receive(requestPacket); //为了接受数据,需要先准备好一个空的DatagramPacket,由receive来填充

  这里的DatagramPacket 表示一个UDP数据报,发送一次数据,就是在发一个DatagramPacket ;接收一次数据,也就是在接收一个DatagramPacket 。此外这里的DatagramPacket 就是把一个字符数组给包装了一下,只不过里面装了更多的东西。

  这里的receive方法是可能会阻塞的,因为我们不清楚客户端什么时候给服务器发送请求。

  由于我们是比较习惯用字符串的,所以这里把这个DatagramPacket解析成一个String。requestPacket.getData()可以拿到客户端传过来的字节数组。requestPacket.getLength()这个获取的长度不一定是1024 ,假设此处的UDP数据报最长是1024,实际的数据可能不够1024

//把DatagramPacket解析成一个String
    String request = new String(requestPacket.getData(),0,requestPacket.getLength(),"UTF-8");

第二步:根据请求计算响应,这里由于是一个回显服务,第二步就省略。

//2.根据请求计算响应,这里由于是一个回显服务,第二步就省略
String response = process(request);//这里的process是一个方法,具体在最后的全部代码里里面

第三步:把响应写回客户端

//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);

  这里send方法的参数,也是 DatagramPacket.需要把响应数据先构造成一个DatagramPacket,再进行发送.所以这里就不是构造一个空的数据报。

  requestPacket.getPort(),这里的参数不再是一个空的字节数组了,response是刚才根据请求计算得到的响应,是非空的,DatagramPacket里面的数据就是String response的数据。

  requestPacket.getSocketAddress()表示把数据发给那个地址和端口,这里的SocketAddress可以视为一个类里面包含了IP和端口的信息。


以下是UdpEchoServer 的完整代码:

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("启动服务器!");
        //UDP不需要建立连接,直接接受从客服端来的数据即可
        while (true){
            //1.读取客户端发来的请求
            DatagramPacket requestPacket = new DatagramPacket(new byte[1024],1024);//需要指定里面的内存空间
            socket.receive(requestPacket); //为了接受数据,需要先准备好一个空的DatagramPacket,由receive来填充
            //把DatagramPacket解析成一个String
            String request = new String(requestPacket.getData(),0,requestPacket.getLength(),"UTF-8");
            //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 server = new UdpEchoServer(9090);
        server.start();
    }
}

2.UdpEchoClient

(1)创建客户端。
在客户端构造socket对象的时候,我们就不再手动指定端口号,而是使用无参版本的构造方法。这里的不指定端口号,意思是,让操作系统自己分配一个空闲的端口号。就比如我们营业厅买一个手机号,自己对是什么手机号没关系,然后营业厅就随机给你一个。

private DatagramSocket socket = null;
    private String serverIP;
    private int serverPort;
    public UdpEchoClient(String ip, int port) throws SocketException {
        // 此处的 port 是服务器的端口.
        // 客户端启动的时候, 不需要给 socket 指定端口. 客户端自己的端口是系统随机分配的~~
        socket = new DatagramSocket();
        serverIP = ip;
        serverPort = port;
    }

  通常写代码的时候,服务器都是手动指定的.客户端都是由系统自动指定的,也就是系统随机分配一个。
对于服务器来说,必须要手动指定,因为后续客户端要根据这个端口来访问到服务器。如果让系统随机分配,客户端就不知道服务器的端口是啥,不能访问
对于客户端来说,如果手动指定也行,但是系统随机分配更好,因为一个机器上的两个进程,不能绑定同一个端口。举个例子:客户端就是普通用户的电脑,我们不知道用户电脑上都装了什么程序,我们也不知道用户的电脑上已经被占用了哪些端口。如果你手动指定一个端口,万一这个端口被别的程序占用,咱们的程序不就不能正常工作了嘛?而且由于客户端是主动发起请求的一方,客户端需要在发送请求之前,先知道服务器的地址和端口.但是反过来在请求发出去之前,服务器是不需要事先知道客户端的地址和端口了。


(2)启动客户端

  1. 先从控制台读取用户输入的字符串
  2. 把这个用户输入的内容, 构造成一个 UDP 请求, 并发送.
  3. 从服务器读取响应数据, 并解析
  4. 把响应结果显示到控制台上.

UdpEchoClient的完整代码:

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.Scanner;

public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String serverIP;
    private int serverPort;
    public UdpEchoClient(String ip, int port) throws SocketException {
        // 此处的 port 是服务器的端口.
        // 客户端启动的时候, 不需要给 socket 指定端口. 客户端自己的端口是系统随机分配的~~
        socket = new DatagramSocket();
        serverIP = ip;
        serverPort = port;
    }

    public void start() throws IOException {
        Scanner scanner = new Scanner(System.in);
        while (true) {
            // 1. 先从控制台读取用户输入的字符串
            System.out.print("-> ");
            String request = scanner.next();
            // 2. 把这个用户输入的内容, 构造成一个 UDP 请求, 并发送.
            //    构造的请求里包含两部分信息:
            //    1) 数据的内容. request 字符串
            //    2) 数据要发给谁~ 服务器的 IP + 端口
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
                    InetAddress.getByName(serverIP), serverPort);
            socket.send(requestPacket);
            // 3. 从服务器读取响应数据, 并解析
            DatagramPacket responsePacket = new DatagramPacket(new byte[1024], 1024);
            socket.receive(responsePacket);
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength(), "UTF-8");
            // 4. 把响应结果显示到控制台上.
            System.out.printf("req: %s, resp: %s\n", request, response);
        }
    }

    public static void main(String[] args) throws IOException {
        // 由于服务器和客户端在同一个机器上, 使用的 IP 仍然是 127.0.0.1 . 如果是在不同的机器上, 当然就需要更改这里的 IP 了
        UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);
        client.start();
    }
}

3.运行两个程序

  这里注意,需要先运行服务器UdpEchoServer,再运行客户端UdpEchoClient。

  当我们运行了UdpEchoServer,虽然这里没有东京,但早就已经把服务器启动起来了,启动了服务器之后,才开始写客户端代码的.在写客户端代码的这个过程中,显然,没人访问服务器的,服务器其实就卡在receive这里,阻塞等待了。

在这里插入图片描述
我们再运行客户端,并且发送数据:
在这里插入图片描述
在这里插入图片描述  上面的52005就是服务器分配的端口了。

  客户端是可以有很多的,一个服务器可以给很多很多客户端提供服务。就比如一个餐馆,可以给很多很多的客人提供就餐服务的。那最多能启动几个呢?

  这取决于服务器的能力了,同一时刻服务器能够处理的客户端的数目存在上限的,服务器处理每个请求,都需要消耗一定的硬件资源,包括不限于,CPU,内存,磁盘,带宽…….能处理多少客户端,取决于:
1.处理一个请求,消耗多少资源

2.机器一共有多少资源能用~

  而在Java中并不容易精确的计算消耗多少资源,JVM里面有很多辅助性的功能,也要消耗额外的资源。实际开发中,通过性能测试的方式,就知道了能有多少个客户端。

  那么我们如何在ieda中启动多个客户端呢?正常情况下,我们需要先关闭上一个客户端才能够重新启动一个新的客户端。那么这里,如下操作,便可打开多个客户端:

在这里插入图片描述
这样就能打开多个了。
在这里插入图片描述


4.翻译功能的UdpEchoClient

  我们写一个带上点业务逻辑的UdpEchoClient,写一个翻译程序(英译汉),请求是一些简单的英文单词,响应也就是英文单词对应的翻译,客户端不变,把服务器代码进行调整.主要是调整process方法,读取请求并解析,把响应写回给客户端,这俩步骤都一样,关键的逻辑就是"根据请求处理响应".

package network;

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

public class UdpDictServer extends UdpEchoServer {
    private HashMap<String ,String> dict = new HashMap<>();
    public UdpDictServer(int port) throws SocketException {
        super(port);
        dict.put("cat","猫猫");
        dict.put("pig","猪猪");
        dict.put("dog","狗狗");
        dict.put("hhh","哈哈");
    }
    @Override
    public String process(String request){
        return dict.getOrDefault(request,"该词无法被翻译");
    }

    public static void main(String[] args) throws IOException {
        UdpDictServer server = new UdpDictServer(9000);
        server.start();
    }


}


二、TCP回显服务

  TCP回显服务与上面UDP不一样的是,这个是需要连接的。

1.TcpEchoServer

第一步:第一步就是要准备好socket文件,这是进行网络编程的大前提

//第一步就是要准备好socket文件,这是进行网络编程的大前提
    private ServerSocket serverSocket = null;
    public TcpEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }

第二步:启动服务器:

public void start() throws IOException {
        System.out.println("服务器启动!");
        while (true) {
            // 由于 TCP 是有连接的, 不能一上来就读数据, 而要先建立连接. (接电话)
            // accept 就是在 "接电话", 接电话的前提是, 有人给你打了, 如果当前没有客户端尝试建立连接, 此处的 accept 就会阻塞.
            // accept 返回了 一个 socket 对象, 称为 clientSocket. 后续和客户端之间的沟通, 都是通过 clientSocket 来完成的.
            // 进一步讲, serverSocket 就干了一件事, 接电话
            Socket clientSocket = serverSocket.accept();
            processConnection(clientSocket);
        }
    }

第三步:处理请求和响应

 private void processConnection(Socket clientSocket) {
        System.out.printf("[%s:%d] 客户端建立连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
        // 接下来来处理请求和响应
        // 这里的针对 TCP socket 的读写就和文件读写是一模一样的!!
        try (InputStream inputStream = clientSocket.getInputStream()) {
            try (OutputStream outputStream = clientSocket.getOutputStream()) {
                // 循环的处理每个请求, 分别返回响应
                Scanner scanner = new Scanner(inputStream);
                while (true) {
                    // 1. 读取请求
                    if (!scanner.hasNext()) {
                        System.out.printf("[%s:%d] 客户端断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
                        break;
                    }
                    // 此处用 Scanner 更方便. 如果不用 Scanner 就用原生的 InputStream 的 read 也是可以的
                    String request = scanner.next();
                    // 2. 根据请求, 计算响应
                    String response = process(request);
                    // 3. 把这个响应返回给客户端
                    // 为了方便起见, 可以使用 PrintWriter 把 OutputStream 包裹一下
                    PrintWriter printWriter = new PrintWriter(outputStream);
                    printWriter.println(response);
                    // 刷新缓冲区, 如果没有这个刷新, 可能客户端就不能第一时间看到响应结果.
                    printWriter.flush();

                    System.out.printf("[%s:%d] req: %s, resp: %s\n", clientSocket.getInetAddress().toString(),
                            clientSocket.getPort(), request, response);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 此处要记得来个关闭操作.
            try {
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

完整代码:



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;


public class TcpEchoServer {
    //第一步就是要准备好socket文件,这是进行网络编程的大前提
    private ServerSocket serverSocket = null;
    public TcpEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动!");
        while (true) {
            // 由于 TCP 是有连接的, 不能一上来就读数据, 而要先建立连接. (接电话)
            // accept 就是在 "接电话", 接电话的前提是, 有人给你打了~~, 如果当前没有客户端尝试建立连接, 此处的 accept 就会阻塞.
            // accept 返回了 一个 socket 对象, 称为 clientSocket. 后续和客户端之间的沟通, 都是通过 clientSocket 来完成的.
            // 进一步讲, serverSocket 就干了一件事, 接电话~~
            Socket clientSocket = serverSocket.accept();
            processConnection(clientSocket);
        }
    }

    private void processConnection(Socket clientSocket) {
        System.out.printf("[%s:%d] 客户端建立连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
        // 接下来来处理请求和响应
        // 这里的针对 TCP socket 的读写就和文件读写是一模一样的!!
        try (InputStream inputStream = clientSocket.getInputStream()) {
            try (OutputStream outputStream = clientSocket.getOutputStream()) {
                // 循环的处理每个请求, 分别返回响应
                Scanner scanner = new Scanner(inputStream);
                while (true) {
                    // 1. 读取请求
                    if (!scanner.hasNext()) {
                        System.out.printf("[%s:%d] 客户端断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
                        break;
                    }
                    // 此处用 Scanner 更方便. 如果不用 Scanner 就用原生的 InputStream 的 read 也是可以的
                    String request = scanner.next();
                    // 2. 根据请求, 计算响应
                    String response = process(request);
                    // 3. 把这个响应返回给客户端
                    // 为了方便起见, 可以使用 PrintWriter 把 OutputStream 包裹一下
                    PrintWriter printWriter = new PrintWriter(outputStream);
                    printWriter.println(response);
                    // 刷新缓冲区, 如果没有这个刷新, 可能客户端就不能第一时间看到响应结果.
                    printWriter.flush();

                    System.out.printf("[%s:%d] req: %s, resp: %s\n", clientSocket.getInetAddress().toString(),
                            clientSocket.getPort(), request, response);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 此处要记得来个关闭操作.
            try {
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private String process(String request) {
        return request;
    }

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

2.TcpEchoClient

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;

public class TcpEchoClient {
    // 用普通的 socket 即可, 不用 ServerSocket 了
    // 此处也不用手动给客户端指定端口号, 让系统自由分配.
    private Socket socket = null;

    public TcpEchoClient(String serverIP, int serverPort) throws IOException {
        // 其实这里是可以给的. 但是这里给了之后, 含义是不同的. ~~
        // 这里传入的 ip 和 端口号 的含义表示的不是自己绑定, 而是表示和这个 ip 端口建立连接!!
        // 调用这个构造方法, 就会和服务器建立连接 (打电话拨号了)
        socket = new Socket(serverIP, serverPort);
    }

    public void start() {
        System.out.println("和服务器连接成功!");
        Scanner scanner = new Scanner(System.in);
        try (InputStream inputStream = socket.getInputStream()) {
            try (OutputStream outputStream = socket.getOutputStream()) {
                while (true) {
                    // 要做的事情, 仍然是四个步骤
                    // 1. 从控制台读取字符串
                    System.out.print("-> ");
                    String request = scanner.next();
                    // 2. 根据读取的字符串, 构造请求, 把请求发给服务器
                    PrintWriter printWriter = new PrintWriter(outputStream);
                    printWriter.println(request);
                    printWriter.flush(); // 如果不刷新, 可能服务器无法及时看到数据.
                    // 3. 从服务器读取响应, 并解析
                    Scanner respScanner = new Scanner(inputStream);
                    String response = respScanner.next();
                    // 4. 把结果显示到控制台上.
                    System.out.printf("req: %s, resp: %s\n", request, response);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws IOException {
        TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
        client.start();
    }
}

目前TCP客户端和服务端实现的功能和UDP差不多,但都存在几个问题:

  1. 对于服务端来说,处理一次请求并返回响应后,才能再次处理下一次请求和响应,效率是比较低的。这个问题比较好解决:可以使用多线程,每次的请求与响应都在线程中处理。这样多个客户端请求的话,可以在多个线程中并发并行的执行。
  2. 服务端解析请求,是只读取了一行,而客户端解析响应,是一直读取到流结束。可以想想为什么解析请求时,没有读取到流结束?

3.多线程版本的服务器

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;

public class TcpThreadEchoServer {
    private ServerSocket serverSocket = null;

    public TcpThreadEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动");
        while (true){
            //由于TCP是有连接的,不能一上来就读数据,而是先建立连接
            //accept就是在接电话,接电话的前提是有人给你打,如果当前没有客户端尝试建立连接,此处的accept就会阻塞
            //accept返回一个socket对象,称为clientSocket,后续和客户端之间的沟通,都是通过clientSocket来完成
            //进一步说,serverSocket就干了一件事,接电话
            Socket clientSocket = serverSocket.accept();
            //改进方法:在这个地方,每次accept成功,都创建一个进程,由新线程负责执行这个processConnection方法
            Thread t = new Thread(()->{
                processConnection(clientSocket);
            });
            t.start();
        }
    }

    private void processConnection(Socket clientSocket) {
        System.out.printf("[%s :%d] 客户端建立连接!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
        //接下来处理请求和响应
        //这里针对TCP socket 的读写就和文件读写一模一样
        try(InputStream inputStream = clientSocket.getInputStream()){
            try(OutputStream outputStream = clientSocket.getOutputStream()){
                //循环的处理每个请求,分别返回响应
                Scanner scanner = new Scanner(inputStream);
                while (true){
                    //1.读取请求
                    if (!scanner.hasNext()){
                        System.out.printf("[%s: %d] 客户端断开连接!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
                        break;
                    }
                    //此处用Scanner更方便,如果不用Scanner就用原生的InputStream的read也是可以的
                    String request = scanner.next();
                    //2.根据请求,计算响应
                    String response = process(request);
                    //3.把这个响应返回给客户端
                    //为了方便起见,可以使用PrintWriter 把 OutputStream包裹一下
                    PrintWriter printWriter = new PrintWriter(outputStream);
                    printWriter.write(response);
                    //刷新缓冲区
                    printWriter.flush();

                    System.out.printf("[%s : %d] req:%s,resp:%s\n" ,clientSocket.getInetAddress().toString(),
                            clientSocket.getPort(),request,response);

                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            //此处记得来个关闭操作
            try {
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            ;
        }
    }

    private String process(String request) {
        return request;
    }

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