服务端实现(多线程版本)
首先我们先设计我们的服务器端,让它可以接收客户端的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文件所在目录
首先执行我们的服务器程序
然后再开另外一个终端代表客户端,执行Client 文件
可以看到一执行我们的Client文件就会自动连接上服务器,并且输出客户端、服务器的IP和端口。
同时我们看到服务器也输出了一条信息
然后我们在客户端输入一串字符串
可以看到在我们输入一串字符串后,服务器马上回发一个我们在代码里面定义的逻辑:返回字符串的长度
然后我们再看看服务器
然后我们在客户端输入“bye”命令,表示终止客户端与服务器的连接客户端输出终止的信息,并结束程序
服务器也输出一条信息
然后我们再次利用客户端发起连接请求
可以看到服务器并没有终止,而是在继续处理其他请求
服务器线程池实现
首先定义我们的线程池:
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);
}
经过测试:我们的服务器同样能够接收到来自客户端的信息
完整服务器代码(线程池)
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请求,然后再在方案里面添加结果树以及结果报告。
在TCP取样器里面定义我们的服务器地址,端口,requestBody信息
我们定义我们的requestBody信息为一行“hello”,一行“bye”
然后我们先设置线程数为100,循环10次,查看两种服务器设计的效率
首先是多线程服务器下的测试结果:
然后是线程池服务器下的测试结果:
可以看到普通多线程下的平均处理时间是16ms,吞吐量为826.4/sec,而线程池的平均处理时间是4ms,吞吐量为955.1/sec
我们进一步加大我们的线程数,把线程池加到100,循环次数不变
首先是多线程服务器下的测试结果:
然后是线程池下的测试结果:
可以看到线程池的吞吐量达到了多线程下吞吐量的两倍
给服务器增加日志功能
首先在我们原本的项目上添加一个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());
我们再次跑起服务器,然后启动一个客户端往服务器里发信息:
然后查看我们的日志文件:
可以看到的日志已经成功记录了服务器启动、客户端连接、客户端信息、客户端断开的信息
我们的日志首先是打印了当前的时间,然后是 INFO 表示这是一个服务器信息,然后是服务器的线程数值,然后是我们的信息