Tcp编程下的多线程任务
标签(空格分隔): tcp 多线程
TCP通信为毛要引入多线程?
基本 TCP 响应服务器一次只能处理一个客户端的请求。当一个客户端向一个已经被其他客户端占用的服务器发送连接请求时,虽然其在连接建立后即可向服务器端发送数据,服务器端在处理完已有客户端的请求前,却不会对新的客户端作出响应,。这种类型的服务器称为”迭代服务器( iterative server) “。迭代服务器按顺序处理客户端的请求,也就是说在完成了对前一客户端的服务后,才会对下一个客户端进行响应。这种服务器最适用于每个客户端所请求的连接时间都被限制在较小范围内的应用中,而对于允许客户端请求长时间服务的情况,后续客户端将面临无法接受的长时间等待。
我们需要一种方法可以独立处理每一个连接,并使它们不会产生相互干扰,而 Java 的多线程技术刚好满足了这一需求,这一机制使服务器能够方便地同时处理多个客户端的请求。通过使用多线程,一个应用程序可以并行执行多项任务,就好像有多个 Java 虚拟机在同时运行。(实际上是多个线程共享了同一个 Java 虚拟机。)
简单来说有以下两种模式:
一客户一线程
为每个连接都创建了一个新的线程来处理。服务器循环执行一些任务,在指定端口上侦听连接,反复接收客户端传入的连接请求,并为每个连接创建一个新的线程来对其进行处理。
public class ServerThread implements Runnable {
private Socket client ;
public ServerThread(Socket client){
this.client = client;
}
//处理通信细节的静态方法,这里主要是方便线程池服务器的调用
public static void execute(Socket client){
try{
BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
//获取Socket的输出流,用来向客户端发送数据
PrintStream out = new PrintStream(client.getOutputStream());
//获取Socket的输入流,用来接收从客户端发送过来的数据
BufferedReader buf = new BufferedReader(new InputStreamReader(client.getInputStream()));
boolean flag =true;
while(flag){
//接收从客户端发送过来的数据
String str = buf.readLine();
System.out.println("服务端接受到的消息:"+str);
if(str == null || "".equals(str)){
flag = false;
}else{
if("bye".equals(str)){
flag = false;
}else{
//将接收到的字符串前面加上echo,发送到对应的客户端
// out.println("echo:" + str);
System.out.println("服务端输入信息:");
String outStr = input.readLine();
out.println(outStr);
}
}
}
out.close();
buf.close();
client.close();
}catch(Exception e){
e.printStackTrace();
}
}
@Override
public void run() {
execute(client);
}
}
public class Server {
public static void main(String[] args) throws Exception {
//服务端在20006端口监听客户端请求的TCP连接
ServerSocket server = new ServerSocket(8086);
int count=0;
Socket client = null;
boolean f = true;
while(f){
//等待客户端的连接,如果没有获取连接
client = server.accept();
count++;
System.out.println("与客户端连接成功!,"+count);
//为每个客户端连接开启一个线程
new Thread(new ServerThread(client)).start();
}
server.close();
}
}
优点:它与迭代服务器非常相似,也是用一个循环来接收和处理客户端的请求。主要不同点在于这种服务器为每个连接创建了一个新的线程来处理,而不是直接处理。(这是可行的,因为 EchoProtocol 类实现了 Runnable 接口。)因此,当多个客户端几乎同时连接服务器时,后请求的客户端不需要等服务器对前面的客户端处理结束后才获得服务,相反,它们看起来是同时接受的服务(虽然比对单一客户端进行服务要稍微慢一些)。
缺点:每个新线程都会消耗系统资源:创建一个线程将占用 CPU 周期,而且每个线程都自己的数据结构(如,栈)也要消耗系统内存。另外,当一个线程阻塞( block)时, JVM 将保存其状态,选择另外一个线程运行,并在上下文转换( context switch)时恢复阻塞线程的状态。随着线程数的增加,线程将消耗越来越多的系统资源。这将最终导致系统花费更多的时间来处理上下文转换和线程管理,更少的时间来对连接进行服务。那种情况下,加入一个额外的线程实际上可能增加客户端总服务时间。
- 线程池
优点:与为每个连接创建一个新的线程不同,服务器在启动时创建一个由固定数量线程组成的线程池( thread pool)。当一个新的客户端连接请求传入服务器,它将交给线程池中的一个线程处理。当该线程处理完这个客户端后,又返回线程池,并为下一次请求处理做好准备。如果连接请求到达服务器时,线程池中的所有线程都已经被占用,它们则在一个队列中等待,直到有空闲的线程可用。
public class ServerPool {
private static final int THREADPOOL_SIZE=2;
private static final int PORT = 8086;
public static void main(String[] args) throws Exception {
final ServerSocket server = new ServerSocket(PORT);
//最多有THREADPOOLSIZE个线程在accept()方法上阻塞等待连接请求
for(int i=0;i<THREADPOOL_SIZE;i++){
Thread thread = new Thread(){
@Override
public void run(){
//线程为某连接提供完服务后,循环等待其他的连接请求
while(true){
//阻塞等待
try {
Socket client = server.accept();
System.out.println("与客户端连接成功");
ServerThread.execute(client);
} catch (IOException e) {
e.printStackTrace();
}
}
}
};
//开启线程
thread.start();
}
}
}
结果如下:
1,服务端设置了线程最大数为2,因此服务端最多可以输出2个“与客户端连接成功”,当第三个同时连接的时候,就会阻塞等待至超时
2,每个线程提前创建,阻塞等待,处理完一个socket连接之后,进入下一次阻塞等待,并不会销毁
3,由于线程的重复使用,线程池的方法只需要付出创建N次线程的系统开销,而与客户端连接总数无关
缺点:线程池大小不好确定,线程数固定不能扩展,出现负载情况不能处理,负载缩减时闲置的多余线程也会消耗系统资源。
- 多线程辅助技能,系统调度管理Executor 接口
在创建线程池时,线程池的大小是个很重要的考虑因素,如果创建的线程太多(空闲线程太多),则会消耗掉很多系统资源,如果创建的线程太少,客户端还是有可能等很长时间才能获得服务。因此,线程池的大小需要根据负载情况进行调整,以使客户端连接的时间最短,理想的情况是有一个调度的工具,可以在系统负载增加时扩展线程池的大小(低于大上限值),负载减轻时缩减线程池的大小。一种解决的方案便是使用 Java 中的 Executor 接口。
interface Executor{
void execute(Runnable task);
}
Java 提供了大量的内置 Executor 接口实现,它们都可以简单方便地使用,ExecutorService 接口继承于Executor 接口,它提供了一个更高级的工具来关闭服务器,包括正常的关闭和突然的关闭。我们可以通过调用Executors 类的各种静态工厂方法来获取 ExecutorService 实例,而后通过调用 execute()方法来为需要处理的任务分配线程,它首先会尝试使用已有的线程,但如果有必要,它会创建一个新的线程来处理任务,另外,如果一个线程空闲了 60 秒以上,则将其移出线程池,而且任务是在 Executor 的内部排队,而不像之前的服务器那样是在网络系统中排队,因此,这个策略几乎总是比前面两种方式实现的 TCP 服务器效率要高。
改进后的服务端:
public class Server {
public static void main(String[] args) throws Exception {
//服务端在20006端口监听客户端请求的TCP连接
ServerSocket server = new ServerSocket(8086);
int count=0;
Socket client = null;
//ExecutorService接口是Executor接口的子接口
Executor service = Executors.newCachedThreadPool();
boolean f = true;
while(f){
//等待客户端的连接,如果没有获取连接
client = server.accept();
count++;
System.out.println("与客户端连接成功!,"+count);
//调用execute()方法时,如果必要,会创建一个新的线程来处理任务,但它首先会尝试使用已有的线程,
//如果一个线程空闲60秒以上,则将其移除线程池;
//另外,任务是在Executor的内部排队,而不是在网络中排队
service.execute(new ServerThread(client));
}
server.close();
}
}