JavaWeb~实现简单的TCP网络服务器与客户端 并使用线程池解决多连接问题

ServerSocket与Socket

  • 简单来说 ServerSocket 处理客户端的连接
  • Socket 进行具体的数据交互

ServerSocket类

ServerSocket(int port) 创建绑定到指定端口的服务器套接字
ServerSocket(int port, int backlog) 创建服务器套接字并将其绑定到指定的本地端口号,并指定了积压。
Socket accept() 侦听要连接到此套接字并接受它
bind(SocketAddress endpoint) 将ServerSocket绑定到特定地址(IP地址和端口号)
InetAddress getInetAddress() 返回此服务器套接字的本地地址
void close() 关闭此套接字
int getLocalPort() 返回此套接字正在侦听的端口号

  • 重要方法 accept()
  1. 三次握手完成后, 服务器调用accept()接受连接;
  2. 如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;
  3. Socket 是其返回值,代表网络的套接字

Socket类

Socket(InetAddress address, int port) 创建流套接字并将其连接到指定IP地址的指定端口号
Socket(String host, int port) 创建流套接字并将其连接到指定主机上的指定端口号
void bind(SocketAddress bindpoint) 将套接字绑定到本地地址
void connect(SocketAddress endpoint) 将此套接字连接到服务器
InetAddress getInetAddress() 返回套接字所连接的地址
InputStream getInputStream() 返回此套接字的输入流
OutputStream getOutputStream() 返回此套接字的输出流

简单的TCP服务器与客户端

  • 服务器
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class TcpEchoServer {
    // 1. 初始化服务器
    // 2. 进入主循环
    //   1) 先去从内核中获取到一个 TCP 的连接
    //   2) 处理这个 TCP 的连接
    //     a) 读取请求并解析
    //     b) 根据请求计算响应
    //     c) 把响应写回给客户端
    private ServerSocket serverSocket = null;

    public TcpEchoServer(int port) throws IOException {
        // 这个操作和前面的 UDP 类似, 也是要绑定端口号.
        serverSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动");
        while (true) {
            // 1) 先从内核中获取到一个 TCP 连接
            Socket clientSocket = serverSocket.accept();
            // 2) 处理这个连接
            processConnection(clientSocket);
        }
    }

    private void processConnection(Socket clientSocket) {
        System.out.printf("[%s:%d] 客户端上线\n", clientSocket.getInetAddress().toString(),
                clientSocket.getPort());
        // 通过 clientSocket 来和客户端交互, 先做好准备工作. 获取到 clientSocket 中的流对象
        try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
             BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))) {
            // 此处咱们先实现一个 "长连接" 版本的服务器.
            // 一次连接的处理过程中, 需要处理多个请求和响应.
            // 这个循环何时结束? 当客户端断开连接时, 就结束了.
            // 当客户端断开连接的时候, 服务器再去调用 readLine 或者 write 方法都会触发异常 (IOException)
            while (true) {
                // 1. 读取请求并解析(此处的 readLine 对应客户端发送数据的格式, 必须是按行发送)
                String request = bufferedReader.readLine();
                // 2. 根据请求计算响应
                String response = process(request);
                // 3. 把响应写回到客户端(客户端要按行来读)
                bufferedWriter.write(response + "\n");
                //因为使用的是带缓冲区的buffer 所以一开始write是写入了缓冲区里 调用flush才可以将数据真正写到socket文件中
                bufferedWriter.flush();
                System.out.printf("[%s:%d] req: %s; resp: %s\n", clientSocket.getInetAddress().toString(),
                        clientSocket.getPort(), request, response);
            }
        } catch (IOException e) {
            // e.printStackTrace();
            System.out.printf("[%s:%d] 客户端下线\n", clientSocket.getInetAddress().toString(),
                    clientSocket.getPort());
        }
    }

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

    public static void main(String[] args) throws IOException {
        TcpEchoServer server = new TcpEchoServer(9090);
        server.start();
    }
}
  • 客户端
import java.io.*;
import java.net.Socket;
import java.util.Scanner;

public class TcpEchoClient {
    // 1. 启动客户端(一定不要绑定端口号) 和服务器建立连接
    // 2. 进入主循环
    //  a) 读取用户输入内容
    //  b) 构造一个请求发送给服务器
    //  c) 读取服务器的响应数据
    //  d) 把响应数据显示到界面上.
    private Socket socket = null;

    public TcpEchoClient(String serverIp, int serverPort) throws IOException {
        // 此处的实例化 Socket 过程, 就是在建立 TCP 连接
        socket = new Socket(serverIp, serverPort);
    }

    public void start() {
        System.out.println("客户端启动");
        Scanner scanner = new Scanner(System.in);
        try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
             BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))) {
            while (true) {
                // 1. 读取用户输入内容
                System.out.print("->");
                String request = scanner.nextLine();
                if ("exit".equals(request)) {
                    break;
                }
                // 2. 构造请求并发送. 此处 + \n 为了和服务器中的 readLine 相对应.
                bufferedWriter.write(request + "\n");
                //因为使用的是带缓冲区的buffer 所以一开始write是写入了缓冲区里 调用flush才可以将数据真正写到socket文件中
                bufferedWriter.flush();
                // 3. 读取响应数据.
                String response = bufferedReader.readLine();
                // 4. 把响应数据显示到界面上.
                System.out.println(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服务器

  • 观察运行上述代码我们发现再启动一个客户端, 尝试连接服务器, 发现第二个客户端, 不能正确的和服务器进行通信.分析原因, 是因为我们accecpt了一个请求之后, 就在一直while循环尝试read, 没有继续调用到accecpt, 导致不能接受新的请求.我们当前的这个TCP, 只能处理一个连接, 这是不科学的
  • 所以我们通过每个请求, 创建子进程的方式来支持多连接
  • 但是还有问题当我们有很多连接的时候 线程就会疯狂的创建和销毁 所以结合前面所学我们可以使用线程池进行优化
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TCPThreadPoolServer {
    //去内核找连接
    //处理这个连接对象
    //获得socket的输入流
    //处理输入流请求
    //将响应写回到socket输出流
    private ServerSocket serverSocket = null;

    //构造函数需要给服务器绑定一个端口

    public TCPThreadPoolServer(int port) throws IOException {
        this.serverSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动");
        //用一个线程池去解决多个多连接问题
        ExecutorService executorService = Executors.newCachedThreadPool();
        while (true) {
            Socket socket = serverSocket.accept();
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    processSocket(socket);
                }

                private void processSocket(Socket socket) {
                    System.out.printf("[%s : %d] 已上线\n", socket.getInetAddress().toString(), socket.getPort());

                    try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                         BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))){

                        while (true) {
                            String get = bufferedReader.readLine();
                            String put = process(get);
                            bufferedWriter.write(put + "\n");
                            //因为使用的是带缓冲区的buffer 所以一开始write是写入了缓冲区里 调用flush才可以将数据真正写到socket文件中
                            bufferedWriter.flush();

                            System.out.printf("[%s:%d] req: %s; resp: %s\n", socket.getInetAddress().toString(),
                                    socket.getPort(), get, put);
                        }

                    } catch (Exception e) {
                        System.out.printf("[%s : %d] 已下线\n", socket.getInetAddress().toString(), socket.getPort());
                    }
                }

                private String process(String get) {
                    return get.toUpperCase();
                }
            });
            
        }
    }

    public static void main(String[] args) {
        try {
            TCPThreadPoolServer tcpThreadPoolServer = new TCPThreadPoolServer(9090);
            tcpThreadPoolServer.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

  • 测试

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 线程池 优点
    利用线程池管理并复用线程、控制最大并发数等。
    实现任务线程队列缓存策略和拒绝机制。
    实现某些与时间相关的功能,如定时执行、周期执行等。
    隔离线程环境。比如,交易服务和搜索服务在同一台服务器上,分别开启两个线程池,交易线程的资源消耗明
    显要大;因此,通过配置独立的线程池,将较慢的交易服务与搜索服务隔开,避免个服务线程互相影响。
  • 线程池 缺点
    前期需要创建多个线程示例对象。
    如果客户端连接少,会造成线程资源浪费

JavaWeb~为什么要引入线程池? 如何自己简单实现一个线程池?

猜你喜欢

转载自blog.csdn.net/Shangxingya/article/details/106938456