NO.71 一连接一线程 : 基于Socket的服务端的多线程模式
Posted 代码荣耀
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了NO.71 一连接一线程 : 基于Socket的服务端的多线程模式相关的知识,希望对你有一定的参考价值。
这是代码荣耀的第 124 篇原创
00、引言
前面我们对Java多线程相关技术点进行了梳理,接下来几篇文章我们将重点讨论多线程在网络编程中的相关应用,同时对Java网络编程相关技术点进行整理。这些技能无论是对于面试,还是提升我们编程能力都大有裨益。下图是其知识体系图。
Fig 1. 知识体系
今天是该系列文章的第一篇,我们首先来看看如何利用Java基于Socket、ServerSocket来实现TCP/IP+BIO的网络通信系统。其中,所谓的BIO就是当发起网络IO的读或写的操作时,均是阻塞方式,只有当程序读到了流或将流写入操作系统后,才能释放资源;Socket主要用于实现建立连接及网络IO的操作,ServerSocket主要用于实现服务器端端口的监听及Socket对象的获取。
01、基本原理
在实际的系统中,通常要面对的是两个问题:
(1) 客户端同时发送多个请求到服务端
(2)服务器端则同时要接受多个连接发送的请求
对于第一个问题:为了满足客户端能同时发送多个请求到服务器端,最简单的方法就是生成多个Socket。但这里会产生两个问题:一是生成太多Socket,会消耗过多的本地资源。在客户端机器多,服务器端机器少的情况下,客户端生成太多Socket,会导致服务器端需要支撑非常高的连接数;二是生成Socket,也就是所谓的建立连接通常是比较慢的,因此频繁的创建会导致系统性能不足。鉴于这两个问题,通常采用连接池的方式来维护Socket是比较好的,一方面限制了能创建Socket的数量,另一方面由于将Socket放入了池中,避免了重复创建Socket带来的性能下降问题。数据库连接池就是这种方式的典型代表,但连接池的方式会带来另一个问题:连接池中的Socket个数是有限的,但同时要用Socket的请求可能会很多,但这种情况下就会造成激烈的竞争和等待;还有一个需要注意的问题是:合理控制等待响应的超时时间,如果不设定超时时间,会导致当服务器处理变慢时,客户端相关的请求都在做无限的等待,而客户端的资源必然是有限的。因此,这种情况下很容易造成当服务器出现问题时,客户端挂掉的现象。超时时间具体设置多少,取决于客户端能承受的请求量和服务器端的处理时间。既要保证性能,又要保证出错率不会过高,通常可采用Socket.setSoTimeout来设置等待响应的超时时间。
对于第二个问题:为了满足服务器端能同时接受多个连接发送的请求,通常采用的方法是在accept获取Socket后,将此Socket放入一个线程中处理,也就是所谓的“一连接一线程”的处理模型,见Fig 2所示;这样服务器端就能接受多个连接的发送请求了。这种方式的缺点是,无论连接上是否有真实的请求,都要耗费一个线程。
Fig 2. 一连接一线程模型
02、典型实现
下面我来举典型例子来实现“一连接一线程”的网络通信模型。
服务器端实现了时间服务,为连接该服务器端的客户端请求发送当前服务器的时间。详见如下代码与注释。
package weixin.test.network;
import java.net.*;
import java.io.*;
import java.util.Date;
//网络服务端提供时间服务
public class MultithreadedDaytimeServer {
// 服务的端口
public final static int PORT = 8899;
public static void main(String[] args) {
//创立连接
try (ServerSocket server = new ServerSocket(PORT)) {
System.out.println("Starting server ... ");
while (true) {
try {
// 接受客户端建立连接的请求,并返回Socket对象,以便和客户端进行交互
Socket connection = server.accept();
// 开启一个线程对新接入的连接进行处理,这是“一线程一连接”实现的关键
Thread task = new DaytimeThread(connection);
task.start();
} catch (IOException ex) {
ex.printStackTrace();
}
}
} catch (IOException ex) {
System.err.println("Start server failed !");
}
}
// 开启一个线程来处理连接的请求
private static class DaytimeThread extends Thread {
private Socket connection;
DaytimeThread(Socket connection) {
this.connection = connection;
}
@Override
public void run() {
try {
Writer out = new OutputStreamWriter(
connection.getOutputStream());
//获取当前时间
Date now = new Date();
//发送给客户端
out.write(now.toString() + "\r\n");
out.flush();
} catch (IOException ex) {
System.err.println(ex);
} finally {
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
客户端主要是向服务器发送请求,获取服务器端当前的时间。详见下述代码与注释。
package weixin.test.network;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.Socket;
//客户端获取服务器端提供的时间服务
public class DayTimeClient {
public static void main(String[] args) throws IOException {
Socket client = null;
BufferedReader reader = null;
// 创建连接
client = new Socket();
try {
// 连接服务器
client.connect(new InetSocketAddress("localhost",8899));
// 创建读取服务器端返回流的BufferedReader
reader = new BufferedReader(new InputStreamReader(client.getInputStream()));
// 阻塞式获取服务器端返回来的信息
System.out.println("Form Server : " + reader.readLine());
} catch (IOException e) {
e.printStackTrace();
} finally{
//非常重要,避免内存泄漏
if(reader != null) reader.close();
if(client != null) client.close();
}
}
}
但是,老铁们先想一想,上述的实现在特定的情况下,是否存在一个致命的问题?
上述服务器上有可能会受到一种拒绝服务攻击。由于服务器端为每个连接都生成了一个线程,大量几乎同时的客户端连接请求可能会导致它生成极大数量的线程。最终,JVM会因耗尽内存而崩溃。一种更好的办法,是使用一种固定的线程池来限制可能的资源使用。这样无论负载有多大,可能会出现拒绝服务(因为服务的数量有限),但是服务器端永远都不会崩溃。其解决方案见Fig.3,老铁们可按照上述实现思想进行练手实践。
03、小结
本文主要对一连接一线程”的网络通信模型原理及实现方法进行了说明。在该模型中,为避免创建过多的线程导致服务器端资源耗尽,需限制创建的线程数量,这就造成了在采用BIO的情况下,服务器端所能支持的连接数是有限的,当然性能也是存在瓶颈的。有没有更好的办法了,肯定有!敬请老铁们期待后续的姊妹篇。
欢迎老铁在留言区写下你的感悟,与大家共同交流。
加入了知识星球伙伴,也可将你按照基于线程池的改进的实现方案写在知识星球的交流区,我会对其实现过程进行点评,相互借鉴,共同提高。
本文延伸阅读
上文1:
上文2:
推荐1:
推荐2:
以上是关于NO.71 一连接一线程 : 基于Socket的服务端的多线程模式的主要内容,如果未能解决你的问题,请参考以下文章
IO多路复用, 基于IO多路复用+socket实现并发请求(一个线程100个请求), 协程