Java多线程技术:实现多用户服务端Socket通信
多用户访问服务器资源,服务器应该与各个客户端建立连接,并进行通信对话,就像我们日常使用QQ、微信、视频等客户端,就是多用户与服务器通信的例子。而上一篇中服务端只实现了单用户的功能,本篇将解决这个问题,详细记录服务端多线程的实现,目标是多用户(客户端)能够同时与服务器建立连接并通信,避免阻塞,进一步完善TCP的Socket网络通信,运用Java多线程技术,实现多用户与服务端Socket通信!
目录
前言回顾
在上一篇《Java多线程实现TCP网络Socket编程(C/S通信)》,我们解决了服务器端在建立连接后,连续发送多条信息给客户端接收的问题,解决办法容易理解,将客户端接收信息的功能集中给线程处理,实现多线程同步进行。
同理,上一篇结束语留下来一个问题,简而言之,相当于多用户访问服务器资源,服务器应该与各个客户端建立连接,并进行通信对话,就像我们日常使用QQ、微信、视频等客户端,就是多用户与服务器通信的例子。
而上一篇中服务端只实现了单用户的功能,本篇将解决这个问题,详细记录服务端多线程的实现,目标是多用户(客户端)能够同时与服务器建立连接并通信,避免阻塞,进一步完善TCP的Socket网络通信,运用Java多线程技术,实现多用户与服务端Socket通信!
Java实现socket通信网络编程系列文章:
一、多用户服务器
多用户服务器是指服务器能同时支持多个用户并发访问服务器所提供的服务资源,如聊天服务、文件传输等。
上一篇的TCPServer是单用户版本,每次只能和一个用户对话。我们可以尝试多用户连接,开启多个客户端,具体操作如下:
这样就允许同时并行执行多个客户端,测试发现,单用户版本的TCPServer.java程序能同时支持多个用户并发连接(TCP三次握手),但不能同时服务多用户对话,只有前一个用户退出后,后面的用户才能完成服务器连接。
多线程技术,线程调用的并行执行。
上一篇提到在java中有两种实现多线程的方法,一是使用Thread类,二是使用Runnable类并实现run()方法。下面将使用Runnable类对服务端相关操作功能进行封装,结合上一篇,就学到了两种多线程实现方法。
//使用Runnable类,作为匿名内部类 class Handler implements Runnable { public void run() { //实现run方法 } }
服务器面临很多客户的并发连接,这种情况的多线程方案一般是:
- 主线程只负责监听客户请求和接受连接请求,用一个线程专门负责和一个客户对话,即一个客户请求成功后,创建一个新线程来专门负责该客户。对于这种方案,可以用上一篇方式new Thread创建线程,但是频繁创建线程需要消耗大量系统资源。所以不采用这种方法。
- 对于服务器,一般使用线程池来管理和复用线程。线程池内部维护了若干个线程,没有任务的时候,这些线程都处于等待状态。如果有新任务,就分配一个空闲线程执行。如果所有线程都处于忙碌状态,新任务要么放入队列等待,要么增加一个新线程进行处理。
显然,我们采用第2种线程池的方法。 常见创建方法如下:
ExecutorService executorService = Executors.newFixedThreadPool(n);//指定线程数量 ExecutorService executorService = Executors.newCachedThreadPool();//动态线程池
接下来就是选择线程池的类型了。 使用第一个固定线程数的线程池,显然不够灵活,第二种方式的线程池会根据任务数量动态调整线程池的大小,作为小并发使用问题不大,但其在实际生产环境使用并不合适,如果并发量过大,常常会引发超出内存错误(OutOfMemoryError),根据我们的应用场景,可以用这个动态调整线程池。
二、使用线程池实现服务端多线程
1、单线程版本
首先,与之前的单线程通信对比一下,下面代码只能实现单用户与服务端通信,如果多用户与服务器通信,则出现阻塞。
//单客户版本,每次只能与一个用户建立通信连接 public void Service(){ while (true){ Socket socket=null; try { //此处程序阻塞,监听并等待用户发起连接,有连接请求就生成一个套接字 socket=serverSocket.accept(); //本地服务器控制台显示客户连接的用户信息 System.out.println("New connection accepted:"+socket.getInetAddress()); BufferedReader br=getReader(socket);//字符串输入流 PrintWriter pw=getWriter(socket);//字符串输出流 pw.println("来自服务器消息:欢迎使用本服务!"); String msg=null; //此处程序阻塞,每次从输入流中读入一行字符串 while ((msg=br.readLine())!=null){ //如果用户发送信息为”bye“,就结束通信 if(msg.equals("bye")){ pw.println("来自服务器消息:服务器断开连接,结束服务!"); System.out.println("客户端离开。"); break; } msg=msg.replace("?","!").replace("?","!") .replace("吗","").replace("吗?","").replace("在","没"); pw.println("来自服务器消息:"+msg); pw.println("来自服务器,重复消息:"+msg); } }catch (IOException e){ e.printStackTrace(); }finally { try { if (socket!=null) socket.close();//关闭socket连接以及相关的输入输出流 }catch (IOException e){ e.printStackTrace(); } } } }
所以,根据上面的分析,将该单线程版本服务端与客户端通信对话的功能独立处理,由一个线程来处理。这样就不会阻塞主进程的执行。具体实现如下面。
2、多线程版本
1、创建匿名内部类Handler,实现Runnable类的run方法,将通信对话放到run()里面:
class Handler implements Runnable { private Socket socket; public Handler(Socket socket) { this.socket = socket; } public void run() { //本地服务器控制台显示客户端连接的用户信息 System.out.println("New connection accept:" + socket.getInetAddress()); try { BufferedReader br = getReader(socket); PrintWriter pw = getWriter(socket); pw.println("From 服务器:欢迎使用服务!"); String msg = null; while ((msg = br.readLine()) != null) { if (msg.trim().equalsIgnoreCase("bye")) { pw.println("From 服务器:服务器已断开连接,结束服务!"); System.out.println("客户端离开。"); break; } pw.println("From 服务器:" + msg); pw.println("来自服务器,重复消息:"+msg); } } catch (IOException e) { e.printStackTrace(); } finally { try { if (socket != null) socket.close(); } catch (IOException e) { e.printStackTrace(); } } } }
2、使用newCachedThreadPool( )动态创建线程池
线程池作为成员变量:
//创建动态线程池,适合小并发量,容易出现OutOfMemoryError private ExecutorService executorService=Executors.newCachedThreadPool();
服务端的Service方法中创建新线程,交给线程池处理。
//多客户版本,可以同时与多用户建立通信连接 public void Service() throws IOException { while (true){ Socket socket=null; socket=serverSocket.accept(); //将服务器和客户端的通信交给线程池处理 Handler handler=new Handler(socket); executorService.execute(handler); } }
三、多用户与服务端通信演示
之前服务端只支持单用户通信对话时候,新用户发送的信息阻塞,服务器无法返回。
很有趣发现一点,另外一端结束通信,与此同时,另一端则立即收到服务器的回复信息。
从显示的时间上初步观察,可以判断之前发送的信息是阻塞在服务端进程,断开一方连接后,服务端才将阻塞队列的信息发送到客户端。那使用多线程之后,结果是怎么样呢?
动图演示进一步体会:
四、多用户服务器完整代码
/* * TCPThreadServer.java * Copyright (c) 2020-11-14 * author : Charzous * All right reserved. */ package chapter05; import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class TCPThreadServer { private int port =8008;//服务器监听窗口 private ServerSocket serverSocket;//定义服务器套接字 //创建动态线程池,适合小并发量,容易出现OutOfMemoryError private ExecutorService executorService=Executors.newCachedThreadPool(); public TCPThreadServer() throws IOException{ serverSocket =new ServerSocket(8008); System.out.println("服务器启动监听在"+port+"端口..."); } private PrintWriter getWriter(Socket socket) throws IOException{ //获得输出流缓冲区的地址 OutputStream socketOut=socket.getOutputStream(); //网络流写出需要使用flush,这里在printWriter构造方法直接设置为自动flush return new PrintWriter(new OutputStreamWriter(socketOut,"utf-8"),true); } private BufferedReader getReader(Socket socket) throws IOException{ //获得输入流缓冲区的地址 InputStream socketIn=socket.getInputStream(); return new BufferedReader(new InputStreamReader(socketIn,"utf-8")); } //多客户版本,可以同时与多用户建立通信连接 public void Service() throws IOException { while (true){ Socket socket=null; socket=serverSocket.accept(); //将服务器和客户端的通信交给线程池处理 Handler handler=new Handler(socket); executorService.execute(handler); } } class Handler implements Runnable { private Socket socket; public Handler(Socket socket) { this.socket = socket; } public void run() { //本地服务器控制台显示客户端连接的用户信息 System.out.println("New connection accept:" + socket.getInetAddress()); try { BufferedReader br = getReader(socket); PrintWriter pw = getWriter(socket); pw.println("From 服务器:欢迎使用服务!"); String msg = null; while ((msg = br.readLine()) != null) { if (msg.trim().equalsIgnoreCase("bye")) { pw.println("From 服务器:服务器已断开连接,结束服务!"); System.out.println("客户端离开。"); break; } pw.println("From 服务器:" + msg); pw.println("来自服务器,重复消息:"+msg); } } catch (IOException e) { e.printStackTrace(); } finally { try { if (socket != null) socket.close(); } catch (IOException e) { e.printStackTrace(); } } } } public static void main(String[] args) throws IOException{ new TCPThreadServer().Service(); } }
最后
本篇将解决了服务端多用户通信的问题,详细记录服务端多线程的实现,目标是多用户(客户端)能够同时与服务器建立连接并通信,避免阻塞,进一步完善TCP的Socket网络通信,运用Java多线程技术,实现多用户与服务端Socket通信!简而言之,相当于多用户访问服务器资源,服务器应该与各个客户端建立连接,就像我们日常使用QQ、微信、视频等客户端,就是多用户与服务器通信的例子。
老问题了,๑乛◡乛๑,好像完成这个之后,可以来实现一个什么有趣的呢?这里停留思考3秒!
……
……
……
就是:实现一个群组聊天房间,类似QQ、微信的群聊,可以多个用户之间的对话交流,是不是感觉挺有趣的。
基于本篇多线程技术实现多用户服务器端的功能,是否能够解决群组聊天房间的功能呢?实现这个功能,等待更新下一篇!
Java实现socket通信网络编程系列文章:
如果觉得不错欢迎“一键三连”哦,点赞收藏关注,有问题直接评论,交流学习!
我的博客园:https://www.cnblogs.com/chenzhenhong/p/13972517.html
我的CSDN博客:https://blog.csdn.net/Charzous/article/details/109440277