NO.71 一连接一线程 : 基于Socket的服务端的多线程模式

Posted 代码荣耀

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了NO.71 一连接一线程 : 基于Socket的服务端的多线程模式相关的知识,希望对你有一定的参考价值。



NO.71 一连接一线程 : 基于Socket的服务端的多线程模式
碎片时间|体系学习
NO.71 一连接一线程 : 基于Socket的服务端的多线程模式

今天是
2018 年的第 80 
这是代码荣耀的第 124 篇原创


00、引言


前面我们对Java多线程相关技术点进行了梳理,接下来几篇文章我们将重点讨论多线程在网络编程中的相关应用,同时对Java网络编程相关技术点进行整理。这些技能无论是对于面试,还是提升我们编程能力都大有裨益。下图是其知识体系图。

NO.71 一连接一线程 : 基于Socket的服务端的多线程模式

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所示;这样服务器端就能接受多个连接的发送请求了。这种方式的缺点是,无论连接上是否有真实的请求,都要耗费一个线程。


NO.71 一连接一线程 : 基于Socket的服务端的多线程模式

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个请求), 协程

socket通信

UrlConnection连接和Socket连接的区别

Socket Server-基于NIO的TCP服务器

Java TCP/IP Socket基于NIO的TCP通信(含代码)

Android 基于UDP的Socket通信