服务端实现(多线程版本)

首先我们先设计我们的服务器端,让它可以接收客户端的socket连接。

首先我们先新创建一个ServerSocket 作为该服务器对应的socket,并且设置它的端口号为2000,创建完毕之后,在控制台输出“准备就绪”的信息,并且打印出ServerSocket 的地址和端口号

ServerSocket serverSocket = new ServerSocket(2000);
System.out.println("服务器准备就绪");
System.out.println("服务器信息:"+serverSocket.getInetAddress()+":"+serverSocket.getLocalPort());

然后我们的服务器就可以开始处理客户端请求了,首先我们设计一个自旋,让服务器每次循环都可以处理一个Client请求。

for(;;){
    Socket client = serverSocket.accept();
    ClientHandler clientHandler = new ClientHandler(client);
    clientHandler.start();

}

在上面的代码中,我们的serverSocket每次都会accept一个client发来的请求并根据该请求创建一个socket,作为client。

然后我们就可以利用该client创建我们的多线程处理器了。在这里我们创建了一个ClientHandler类继承自Thread类,作为一个线程类。每次ClientHandler类会根据client创建一个clientHandler对象,clientHandler.start()就会开启一个线程用于处理客户端请求。

下面我将对ClientHandler类进行定义:

private static class ClientHandler extends Thread {
    //接收来自一个请求的socket
    private Socket socket;
    //终止标志
    private boolean flag ;

    //接收一个socket进行构造,并且把循环标志设为true
    ClientHandler(Socket socket){
        this.socket = socket;
        flag = true;
    }

    //重写run方法
    @Override
    public void run(){
        super.run();
        System.out.println("新客户端连接到了服务器:"+socket.getInetAddress()+":"+socket.getPort());

        try {
            //打印流,用于服务器回送数据
            PrintStream socketOutput = new PrintStream(socket.getOutputStream());
            //得到输入流,用于接收数据
            BufferedReader socketInput = new BufferedReader(new InputStreamReader(socket.getInputStream()));

            //自旋,当收到客户端传来的"bye"字符串时才终止
            while (flag){
                //接收客户端发来的一行requestBody
                String str = socketInput.readLine();
                if(str!=null){
                    //如果客户端发来的是"bye",就把flag设置false,关闭资源
                    if("bye".equalsIgnoreCase(str)){
                        flag=  false;
                        socketOutput.println("客户端:"+socket.getInetAddress()+"请求关闭连接");
                    }else{
                        //在控制台输出客户端发来的信息以及字符
                        System.out.println("服务器接收到来自客户端"+socket.getInetAddress()+"的信息:"+str);
                        //返回客户端字符串的长度
                        socketOutput.println(str.length());
                    }
                }

            }
            //关闭IO
            socketInput.close();
            socketOutput.close();

        } catch (Exception e){
            e.printStackTrace();
            System.out.println("连接异常");
        } finally {
            try {
                //关闭socket连接
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            System.out.println("客户端"+socket.getInetAddress()+"断开连接");
        }

    }
}

客户端设计

服务端设计完了,下面我们将设计我们的客户端,让客户端可以与服务端连接,并且可以互相发送信息

首先在main方法定义我们的socket,让它与服务器进行连接

//定于客户端socket
Socket socket = new Socket();
//设置连接超时时间为3000ms
socket.setSoTimeout(3000);
//连接本地服务器,端口号为2000,超时时间为3000
socket.connect(new InetSocketAddress(InetAddress.getLocalHost(),2000),3000);

连接完成之后我们在控制台输入信息,打印客户端、服务器的IP和端口
System.out.println("已经发起服务器连接");
System.out.println("客户端Ip:"+socket.getLocalAddress()+"端口:"+socket.getLocalPort());
System.out.println("服务器Ip:"+socket.getInetAddress()+"端口"+socket.getPort());

然后就可以开始我们的客户端会话请求操作,我们这里定义了一个send(Socket socket)函数来让一个socket发送信息到服务器

在send函数里面,我们定义一个键盘输入流用来接收键盘输入数据,再定义一个socket输出流,用于发送数据到服务器,再定义一个socket输入流,用于接收来自服务器的消息。

定义完毕后就可以开始我们的操作:键盘输入的数据发送到服务器,然后接收服务器回发的message,在控制台输出。如果我们输入的一行数据为“bye”,就关闭socket连接,结束我们的客户端主进程。

send(Socket socket)函数

private static void send(Socket client) throws IOException {
    //键盘输入流
    InputStream in = System.in;
    BufferedReader input = new BufferedReader(new InputStreamReader(in));

    //socket输出流
    OutputStream outputStream = client.getOutputStream();
    PrintStream socketPrintStream = new PrintStream(outputStream);

    //获取socket输入流
    InputStream inputStream = client.getInputStream();
    BufferedReader socketBufferReader = new BufferedReader(new InputStreamReader(inputStream));

    boolean flag = true;

    while (flag){
        //键盘读取一行
        String str = input.readLine();
        //发送到服务器
        socketPrintStream.println(str);

        //从服务器读取一行
        String echo = socketBufferReader.readLine();
        if(echo.equalsIgnoreCase("bye"))
            flag = false;
        else
            System.out.println(echo);

    }
}


在main方法里面关闭资源

try {
    send(socket);
} catch (Exception e){
    e.printStackTrace();
    System.out.println("操作出错");
}
socket.close();
System.out.println("客户端终止连接");

完整的服务器与客户端代码

服务器完整代码(多线程)

import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.Executor;

public class Server {

    private static final Logger log = Logger.getLogger(Server.class);

    public static void main(String[] args) throws IOException {




        ServerSocket serverSocket = new ServerSocket(2000);
        System.out.println("服务器准备就绪,当前启动方式是普通多线程启动");
        System.out.println("服务器信息:"+serverSocket.getInetAddress()+":"+serverSocket.getLocalPort());

        log.info("服务器准备就绪");
        log.info("服务器信息:"+serverSocket.getInetAddress()+":"+serverSocket.getLocalPort());

        for(;;){
            Socket client = serverSocket.accept();
            ClientHandler clientHandler = new ClientHandler(client);
            clientHandler.start();

        }
    }


    private static class ClientHandler extends Thread {
        //接收来自一个请求的socket
        private Socket socket;
        //终止标志
        private boolean flag ;

        //接收一个socket进行构造,并且把循环标志设为true
        ClientHandler(Socket socket){
            this.socket = socket;
            flag = true;
        }

        //重写run方法
        @Override
        public void run(){
            super.run();

            log.info("新客户端连接到了服务器:"+socket.getInetAddress()+":"+socket.getPort());
            System.out.println("新客户端连接到了服务器:"+socket.getInetAddress()+":"+socket.getPort());

            try {
                //打印流,用于服务器回送数据
                PrintStream socketOutput = new PrintStream(socket.getOutputStream());
                //得到输入流,用于接收数据
                BufferedReader socketInput = new BufferedReader(new InputStreamReader(socket.getInputStream()));

                //自旋,当收到客户端传来的"bye"字符串时才终止
                while (flag){
                    //接收客户端发来的一行requestBody
                    String str = socketInput.readLine();
                    if(str!=null){
                        //如果客户端发来的是"bye",就把flag设置false,关闭资源
                        if("bye".equalsIgnoreCase(str)){
                            flag=  false;
                            socketOutput.println("客户端:"+socket.getInetAddress()+"请求关闭连接");
                        }else{
                            //在控制台输出客户端发来的信息以及字符
                            log.info("服务器接收到来自客户端"+socket.getInetAddress()+"的信息:"+str);
                            System.out.println("服务器接收到来自客户端"+socket.getInetAddress()+"的信息:"+str);
                            //返回客户端字符串的长度
                            socketOutput.println(str.length());
                        }
                    }

                }
                //关闭IO
                socketInput.close();
                socketOutput.close();

            } catch (Exception e){
                e.printStackTrace();
                log.error("连接异常");
                log.error(e.getMessage());
                System.out.println("连接异常");
            } finally {
                try {
                    //关闭socket连接
                    socket.close();
                } catch (IOException e) {
                    log.error("连接异常");
                    log.error(e.getMessage());
                }
                System.out.println("客户端"+socket.getInetAddress()+"断开连接");
                log.info("客户端"+socket.getInetAddress()+"断开连接");
            }

        }
    }
}

客户端完整代码

import java.io.*;
import java.net.*;

public class Client {
    public static void main(String[] args) throws IOException {

        //定于客户端socket
        Socket socket = new Socket();
        //设置连接超时时间为3000ms
        socket.setSoTimeout(3000);
        //连接本地服务器,端口号为2000,超时时间为3000
        socket.connect(new InetSocketAddress(InetAddress.getLocalHost(),2000),3000);

        System.out.println("已经发起服务器连接");
        System.out.println("客户端Ip:"+socket.getLocalAddress()+"端口:"+socket.getLocalPort());
        System.out.println("服务器Ip:"+socket.getInetAddress()+"端口"+socket.getPort());

        try {
            send(socket);
        } catch (Exception e){
            e.printStackTrace();
            System.out.println("操作出错");
        }
        socket.close();
        System.out.println("客户端终止连接");
    }
    private static void send(Socket client) throws IOException {
        //键盘输入流
        InputStream in = System.in;
        BufferedReader input = new BufferedReader(new InputStreamReader(in));

        //socket输出流
        OutputStream outputStream = client.getOutputStream();
        PrintStream socketPrintStream = new PrintStream(outputStream);

        //获取socket输入流
        InputStream inputStream = client.getInputStream();
        BufferedReader socketBufferReader = new BufferedReader(new InputStreamReader(inputStream));

        boolean flag = true;

        while (flag){
            //键盘读取一行
            String str = input.readLine();
            //发送到服务器
            socketPrintStream.println(str);

            //从服务器读取一行
            String echo = socketBufferReader.readLine();
            if(echo.equalsIgnoreCase("bye"))
                flag = false;
            else
                System.out.println(echo);

        }
    }

}

客户端连接服务器

然后就可以编译我们的程序,得到我们的class文件

进入mac下的终端,cd到我们到class文件所在目录

java 连接池监控 java socket 连接池_socket

首先执行我们的服务器程序

java 连接池监控 java socket 连接池_java 连接池监控_02


然后再开另外一个终端代表客户端,执行Client 文件

java 连接池监控 java socket 连接池_客户端_03


可以看到一执行我们的Client文件就会自动连接上服务器,并且输出客户端、服务器的IP和端口。

同时我们看到服务器也输出了一条信息

java 连接池监控 java socket 连接池_客户端_04

然后我们在客户端输入一串字符串

java 连接池监控 java socket 连接池_java_05

可以看到在我们输入一串字符串后,服务器马上回发一个我们在代码里面定义的逻辑:返回字符串的长度

然后我们再看看服务器

java 连接池监控 java socket 连接池_socket_06


然后我们在客户端输入“bye”命令,表示终止客户端与服务器的连接客户端输出终止的信息,并结束程序

java 连接池监控 java socket 连接池_java_07

服务器也输出一条信息

java 连接池监控 java socket 连接池_服务器_08


然后我们再次利用客户端发起连接请求

java 连接池监控 java socket 连接池_java_09


java 连接池监控 java socket 连接池_java 连接池监控_10

可以看到服务器并没有终止,而是在继续处理其他请求

服务器线程池实现

首先定义我们的线程池:

ExecutorService executorService = Executors.newFixedThreadPool(30);

线程池有以下主要构造函数:

  • corePoolSize:核心线程数
  • maxPoolSize:最大线程数
  • keepAliveTime:保持存活时间,超出corePoolSize的线程超过Time会取消
  • workQueue:任务存储队列

其中创建线程池的常用方式有

  • newFixedThreadPool:创建固定容量的线程池,核心线程数和最大线程数一致
  • newSingleThreadExecutor:创建单线程线程池
  • newCacheThreadExecutor:创建一个最大线程数为Integer.MAX_VALUE的线程池

在这里我们仅仅是创建一个拥有10个核心线程数的线程池就可以。
其余我们基本跟方法一一致,不同的是我们利用一个socket来创建的是对象是implements了Runnable的而不是继承自Thread类

private static class Task implements Runnable

Task(Socket socket){
    this.socket = socket;
    flag = true;
}

然后我们在服务器自旋里根据接收到的socket创建一个Task,然后让线程池去执行就可以

for(;;){
    Socket client = serverSocket.accept();
    Task task = new Task(client);
    executorService.submit(task);
}

经过测试:我们的服务器同样能够接收到来自客户端的信息

java 连接池监控 java socket 连接池_java 连接池监控_11

完整服务器代码(线程池)

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ServerExecutor {

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(2000);
        System.out.println("服务器准备就绪");
        System.out.println("服务器信息:"+serverSocket.getInetAddress()+":"+serverSocket.getLocalPort());

        //创建线程池
        ExecutorService executorService  =   Executors.newFixedThreadPool(30);
        for(;;){
            Socket client = serverSocket.accept();
            Task task = new Task(client);
            executorService.submit(task);
        }
    }

    private static class Task implements Runnable{

        private Socket socket;
        private boolean flag ;

        Task(Socket socket){
            this.socket = socket;
            flag = true;
        }

        @Override
        public void run() {
            System.out.println("新客户端连接到了服务器:"+socket.getInetAddress()+":"+socket.getPort());

            try {
                //打印流,用于服务器回送数据
                PrintStream socketOutput = new PrintStream(socket.getOutputStream());
                //得到输入流,用于接收数据
                BufferedReader socketInput = new BufferedReader(new InputStreamReader(socket.getInputStream()));

                while (flag){
                    String str = socketInput.readLine();
                    if(str!=null){
                        if("bye".equalsIgnoreCase(str)){
                            flag=  false;
                            socketOutput.println("客户端与服务器连接结束");
                        }else{
                            System.out.println("服务器接收到来自客户端"+socket.getInetAddress()+"的信息:"+str);
                            socketOutput.println(str.length());
                        }
                    }
                }
                socketInput.close();
                socketOutput.close();

            } catch (Exception e){
                System.out.println("连接异常");
            } finally {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                System.out.println("客户端"+socket.getInetAddress()+"断开连接");
            }

        }
    }


}

利用JMeter对多线程与线程池服务器进行压测对比

我们利用原来的服务器端代码进行对比

下面我们测试利用的是JMeter压测工具,很多高并发项目都是利用JMeter工具进行压测

我们在Jmeter里面新建一个测试方案,在方案里面添加一个TCP取样器,用来发送TCP请求,然后再在方案里面添加结果树以及结果报告。

java 连接池监控 java socket 连接池_客户端_12

在TCP取样器里面定义我们的服务器地址,端口,requestBody信息
我们定义我们的requestBody信息为一行“hello”,一行“bye”

java 连接池监控 java socket 连接池_java_13

然后我们先设置线程数为100,循环10次,查看两种服务器设计的效率

java 连接池监控 java socket 连接池_服务器_14

首先是多线程服务器下的测试结果:

java 连接池监控 java socket 连接池_服务器_15


然后是线程池服务器下的测试结果:

java 连接池监控 java socket 连接池_客户端_16

可以看到普通多线程下的平均处理时间是16ms,吞吐量为826.4/sec,而线程池的平均处理时间是4ms,吞吐量为955.1/sec

我们进一步加大我们的线程数,把线程池加到100,循环次数不变

首先是多线程服务器下的测试结果:

java 连接池监控 java socket 连接池_客户端_17

然后是线程池下的测试结果:

java 连接池监控 java socket 连接池_java_18

可以看到线程池的吞吐量达到了多线程下吞吐量的两倍

给服务器增加日志功能

首先在我们原本的项目上添加一个pom.xml,使其成为一个maven项目

然后在pom.xml里面添加slf4j日志的依赖

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>4.1.6.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>1.7.25</version>
    </dependency>
</dependencies>

我们的依赖导入完成之后,还需要配置我们的日志
在main/java/resources下面创建log4j.properties,配置我们的日志

log4j.rootLogger=debug, stdout,file

# Redirect log messages to console
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n

# Rirect log messages to a log file
log4j.appender.file=org.apache.log4j.RollingFileAppender
log4j.appender.file.File=ServiceOut.log
log4j.appender.file.MaxFileSize=5MB
log4j.appender.file.MaxBackupIndex=10
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n

表示我们的日志有控制台输出和文件输出的方法,然后重要的参数有:

  • log4j.appender.file.File:表示我们的日志文件名
  • log4j.appender.file.layout.ConversionPattern:表示我们的日志格式

配置完毕后就可以在我们的服务器代码里面添加日志了
首先在main函数前面添加一个定义我们的logger

private static final Logger log = Logger.getLogger(Server.class);

然后把我们之前的输出语句换成log.info()
括号内容是要打印的内容,里面已经包含了我们的时间信息,所以我们的info日志要打印的如下:

服务器servicesocket创建成功:

log.info("服务器准备就绪");
log.info("服务器信息:"+serverSocket.getInetAddress()+":"+serverSocket.getLocalPort());

有新的客户端连接:

log.info("新客户端连接到了服务器:"+socket.getInetAddress()+":"+socket.getPort());

接收到客户端的信息:

log.info("服务器接收到来自客户端"+socket.getInetAddress()+"的信息:"+str);

出现异常:

log.error("连接异常");
log.error(e.getMessage());

我们再次跑起服务器,然后启动一个客户端往服务器里发信息:

java 连接池监控 java socket 连接池_java 连接池监控_19

然后查看我们的日志文件:

java 连接池监控 java socket 连接池_socket_20

可以看到的日志已经成功记录了服务器启动、客户端连接、客户端信息、客户端断开的信息

java 连接池监控 java socket 连接池_java 连接池监控_21

我们的日志首先是打印了当前的时间,然后是 INFO 表示这是一个服务器信息,然后是服务器的线程数值,然后是我们的信息