在我第一篇博客给出的例子中,EchoServer接收到一个客户端连接,就与客户进行通信,通信完毕后断开连接,然后再接收下一个客户端的连接。而如果有多个客户端请求连接,那么这些客户端就需要排队等候。
在实际应用中,一个服务器往往同时为多个客户端提供服务,还是以HTTP服务器为例,HTTP服务器可能同时收到大量客户的请求,每个客户都希望尽快得到响应,如果让客户等待时间过长,就是造成网站客户量流失,访问量下降等严重后果
通常我们用并发性能,来衡量一个服务器同时响应多个客户的能力,一个优秀的服务器,应该满足一下两点:
(1)能同时处理大量请求
(2)对于每个客户,都能迅速给予响应。
用多个线程来为多个客户提供服务,是提高并发性能的通用手段。
这篇文章中我会通过几个实例由浅入深来说一说如何实现多线程的套接字通信:
例1:为每个客户分配一个线程
服务器主线程负责与接收客户端连接,每接到一个连接,就会创建一个工作线程,由工作线程负责与客户端通信,在EchoServer类中:
public void service() {
while (true) {
Socket socket = null;
try {
socket = serverSocket.accept();
Thread workThread = new Thread(new Handler(socket));
workThread.start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
以上方法中,将接收到的每一个连接请求都交给一个新线程的Handle。Handle类实现了Runable接口,在重写的run()方法中进行与客户端交换数据。
在EchoClient的talk()方法中:
public void talk() throws IOException {
for (int i = 0; i < 100; i++) {
try {
socket = new Socket(host, port);
BufferedReader br = getReader(socket);
PrintWriter pw = getWriter(socket);
String msg = "你好,我是第" + i + "个客户端";
pw.println(i);
System.out.println(br.readLine());
} catch (IOException e) {
e.printStackTrace();
} finally {
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
我们通过for循环,向服务器发送100此连接请求,并每次都向服务器发送不同的数据。
以下为完整代码:
EchoService:
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class EchoServer {
private int port = 8001;
private ServerSocket serverSocket;
public EchoServer() throws IOException {
serverSocket = new ServerSocket(port);
System.out.println("服务器启动");
}
public void service() {
while (true) {
Socket socket = null;
try {
socket = serverSocket.accept(); //创建Socket对象,接收客户端的连接
Thread workThread = new Thread(new Handler(socket)); //将接收到的每一个连接请求都交给一个新线程的Handle
workThread.start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws IOException {
new EchoServer().service();
}
}
class Handler implements Runnable { //处理Socket连接的handle类
private Socket socket = null;
public Handler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
System.out.print("新的连接已建立,");
System.out.print("客户端IP" + socket.getInetAddress()); //打印客户端地址
System.out.print(",客户端端口号" + socket.getPort()); //打印客户端端口号
BufferedReader br = getReader(socket);
PrintWriter pw = getWriter(socket);
String msg = null;
while ((msg = br.readLine()) != null) {
System.out.println(msg); //打印客户端发来的数据
pw.println(echo(msg)); //给客户端返回数据
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if(socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public String echo(String msg) {
return "服务器传回的数据:" + msg;
}
private PrintWriter getWriter(Socket socket) throws IOException {
OutputStream out = socket.getOutputStream();
return new PrintWriter(out, true);
}
private BufferedReader getReader(Socket socket) throws IOException {
InputStream in = socket.getInputStream();
return new BufferedReader(new InputStreamReader(in));
}
}
EchoClient:
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
public class EchoClient extends Thread{
private String host = "localhost";
private int port = 8001;
private Socket socket = null;
private PrintWriter getWriter(Socket socket) throws IOException {
OutputStream out = socket.getOutputStream();
return new PrintWriter(out, true);
}
private BufferedReader getReader(Socket socket) throws IOException {
InputStream in = socket.getInputStream();
return new BufferedReader(new InputStreamReader(in));
}
public void talk() throws IOException {
for (int i = 0; i < 100; i++) { //发起100次连接请求
try {
socket = new Socket(host, port); //创建Socket对象,向服务器发起连接请求
BufferedReader br = getReader(socket);
PrintWriter pw = getWriter(socket);
String msg = "你好,我是第" + i + "个客户端";
pw.println(msg); //向服务器发送数据
System.out.println(br.readLine()); //打印服务器传回的数据
} catch (IOException e) {
e.printStackTrace();
} finally {
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
public static void main(String[] args) throws IOException {
new EchoClient().talk();
}
}
先执行服务器的主方法,再执行客户端的主方法,出现以下运行结果:
在上述例子中,虽然解决了单服务器与多客户端连接的问题,但是有以下几点不足:
(1)创建和销毁线程的开销十分大,上述例子中,我们的通信时间很短,导致为了通信而造成的开销比通信本身花费的开销还大,得不偿失。
(2)除了创建和销毁占用资源,活动的线程也消耗资源,每个线程占用约1MB,上述我们才100个请求,而实际当并发量很高的时候,这是一个非常大的开销。
(3)正常情况下,切换线程(在操作系统底层调度下,线程之间转让CPU使用权)的切换周期大概为20ms,而频繁的创建和销毁会导致线程之间的切换不遵循固定的切换周期,线程切换的开销甚至比创建和销毁线程的开销还大。
为了解决这些问题,就需要引入线程池的概念,线程池有以下几点好处:
(1)减少了创建和销毁的次数,每一个线程都可以被重用,执行多个任务。
(2)可以根据系统的承载能力,方便的调整线程的数目,防止系统过载。
例2:通过线程池实现套接字
java.util.concurr包中提供了线程池的实现,篇幅原因这里不详细说jdk的线程池如何实现,只是通过具体的例子演示如何用线程池实现C/S通信,服务器的代码实现和上面的例子差不多,只是改变了多线程处理socket对象的方法:
private ExecutorService executorService;
private final int POOL_SIZE = 4;
public EchoServer() throws IOException {
//创建线程池,这里线程个数的思想是CPU越多,开的线程越多,
Runtime.getRuntime().availableProcessors()返回的是当前操作系统的CPU数目
executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * POOL_SIZE);
}
以上代码创建线程池,并规定开的线程的个数。
public void service() {
while (true) {
Socket socket = null;
try {
socket = serverSocket.accept(); //创建Socket对象,接收客户端的连接
executorService.execute(new Handler(socket)); //将接收到的每一个连接请求都交给线程池处理
} catch (IOException e) {
e.printStackTrace();
}
}
}
在service()方法中使用线程池, 调用excute(
Runable task)方法执行Runable型任务。
以下为用线程池实现的服务器的代码:
EchoServer:
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RunnableFuture;
public class EchoServer {
private int port = 8001;
private ServerSocket serverSocket;
private ExecutorService executorService;
private final int POOL_SIZE = 4;
public EchoServer() throws IOException {
serverSocket = new ServerSocket(port);
System.out.println("服务器启动");
//创建线程池,这里线程个数的思想是CPU越多,开的线程越多,Runtime.getRuntime().availableProcessors()返回的是当前操作系统的CPU数目
executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * POOL_SIZE);
}
public void service() {
while (true) {
Socket socket = null;
try {
socket = serverSocket.accept(); //创建Socket对象,接收客户端的连接
executorService.execute(new Handler(socket)); //将接收到的每一个连接请求都交给线程池处理
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws IOException {
new EchoServer().service();
}
}
class Handler implements Runnable { //处理Socket连接的handle类
private Socket socket = null;
public Handler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
System.out.print("新的连接已建立,");
System.out.print("客户端IP" + socket.getInetAddress()); //打印客户端地址
System.out.print(",客户端端口号" + socket.getPort()); //打印客户端端口号
BufferedReader br = getReader(socket);
PrintWriter pw = getWriter(socket);
String msg = null;
while ((msg = br.readLine()) != null) {
System.out.println(msg); //打印客户端发来的数据
pw.println(echo(msg)); //给客户端返回数据
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public String echo(String msg) {
return "服务器传回的数据:" + msg;
}
private PrintWriter getWriter(Socket socket) throws IOException {
OutputStream out = socket.getOutputStream();
return new PrintWriter(out, true);
}
private BufferedReader getReader(Socket socket) throws IOException {
InputStream in = socket.getInputStream();
return new BufferedReader(new InputStreamReader(in));
}
}
使用这个客户端的运行结果和例1相同,但是却大大的节省了系统资源。