目录

前言回顾

一、多用户服务器

二、使用线程池实现服务端多线程

1、单线程版本

2、多线程版本

三、多用户与服务端通信演示

四、多用户服务器完整代码

最后


前言回顾

在上一篇《Java多线程实现TCP网络Socket编程(C/S通信)》,我们解决了服务器端在建立连接后,连续发送多条信息给客户端接收的问题,解决办法容易理解,将客户端接收信息的功能集中给线程处理,实现多线程同步进行。

同理,上一篇结束语留下来一个问题,简而言之,相当于多用户访问服务器资源,服务器应该与各个客户端建立连接,并进行通信对话,就像我们日常使用QQ、微信、视频等客户端,就是多用户与服务器通信的例子。

而上一篇中服务端只实现了单用户的功能,本篇将解决这个问题,详细记录服务端多线程的实现,目标是多用户(客户端)能够同时与服务器建立连接并通信,避免阻塞,进一步完善TCP的Socket网络通信,运用Java多线程技术,实现多用户与服务端Socket通信!

Java实现socket通信网络编程系列文章:

    1. UDP协议网络Socket编程(java实现C/S通信案例)
    2. Java:基于TCP协议网络socket编程(实现C/S通信)
    3. Java多线程实现TCP网络Socket编程(C/S通信)

一、多用户服务器

多用户服务器是指服务器能同时支持多个用户并发访问服务器所提供的服务资源,如聊天服务、文件传输等。

上一篇的TCPServer是单用户版本,每次只能和一个用户对话。我们可以尝试多用户连接,开启多个客户端,具体操作如下:

这样就允许同时并行执行多个客户端,测试发现,单用户版本的TCPServer.java程序能同时支持多个用户并发连接(TCP三次握手),但不能同时服务多用户对话,只有前一个用户退出后,后面的用户才能完成服务器连接。

多线程技术,线程调用的并行执行。

上一篇提到在java中有两种实现多线程的方法,一是使用Thread类,二是使用Runnable类并实现run()方法。下面将使用Runnable类对服务端相关操作功能进行封装,结合上一篇,就学到了两种多线程实现方法。

//使用Runnable类,作为匿名内部类
class Handler implements Runnable {
public void run() {
//实现run方法
}
}

服务器面临很多客户的并发连接,这种情况的多线程方案一般是:

  1. 主线程只负责监听客户请求和接受连接请求,用一个线程专门负责和一个客户对话,即一个客户请求成功后,创建一个新线程来专门负责该客户。对于这种方案,可以用上一篇方式new Thread创建线程,但是频繁创建线程需要消耗大量系统资源。所以不采用这种方法。
  2. 对于服务器,一般使用线程池来管理和复用线程。线程池内部维护了若干个线程,没有任务的时候,这些线程都处于等待状态。如果有新任务,就分配一个空闲线程执行。如果所有线程都处于忙碌状态,新任务要么放入队列等待,要么增加一个新线程进行处理。

显然,我们采用第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通信网络编程系列文章:

  1. UDP协议网络Socket编程(java实现C/S通信案例)
  2. Java:基于TCP协议网络socket编程(实现C/S通信)
  3. Java多线程实现TCP网络Socket编程(C/S通信)

如果觉得不错欢迎“一键三连”哦,点赞收藏关注,有问题直接评论,交流学习!

我的博客园:https://www.cnblogs.com/chenzhenhong/p/13972517.html

我的CSDN博客:https://blog.csdn.net/Charzous/article/details/109440277


版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。

Java多线程技术:实现多用户服务端Socket通信的更多相关文章

  1. 客户端技术:Cookie 服务端技术:HttpSession

    客户端技术:Cookie 服务端技术:HttpSession 07. 五 / android基础 / 没有评论   一.会话技术1.什么是会话:客户打开浏览器访问一个网站,访问完毕之后,关闭浏览器.这 ...

  2. 赶紧收藏!王者级别的Java多线程技术笔记,我java小菜鸡愿奉你为地表最强!

    Java多线程技术概述 介绍多线程之前要介绍线程,介绍线程则离不开进程. 首先 , 进程 :是一个正在执行中的程序,每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元: 线程:就 ...

  3. socket模块实现基于UDP聊天模拟程序;socketserver模块实现服务端 socket客户端代码示例

    socket模块 serSocket.setblocking(False) 设置为非阻塞: #coding=utf-8 from socket import * import time # 用来存储所 ...

  4. 用PHP的socket实现客户端到服务端的通信

    服务端 <?php error_reporting(E_ALL); set_time_limit(0); ob_implicit_flush(); //本地IP $address = 'loca ...

  5. Java多线程技术学习笔记(二)

    目录: 线程间的通信示例 等待唤醒机制 等待唤醒机制的优化 线程间通信经典问题:多生产者多消费者问题 多生产多消费问题的解决 JDK1.5之后的新加锁方式 多生产多消费问题的新解决办法 sleep和w ...

  6. tcp服务端socket

    import socket if __name__ == '__main__': # 创建tcp服务端socket tcp_server_socket = socket.socket(socket.A ...

  7. linux网络编程之用socket实现简单客户端和服务端的通信(基于UDP)

    单客户端和服务端的通信(基于UDP)   代码 服务端代码socket3.c #include<sys/types.h> #include<sys/socket.h> #inc ...

  8. Java 实现Redis客户端,服务端

    Java 实现Redis客户端,服务端 1.Java实现Redis发布订阅 1.1实例 2.[Redis]Java实现redis消息订阅/发布(PubSub) 3.java实现 redis的发布订阅 ...

  9. 自己用java实现飞鸽传书 1 - 实现socket通信

    第一步: 建立服务端客户端,实现端到端通信.因为要传递文件,信号量较大,故使用TCP/IP协议. 服务端和客户端都要建立socket,而后通过socket进行通信.目前只实现服务端到客户端的单向通信. ...

随机推荐

  1. vue超出8个字符,显示省略号

    显示的数据

  2. python`最简单的爬虫`实现

    不管怎么样,一天一更的好习惯一定要保持,现在一天不写点东西都感觉不踏实,总会感觉少了点什么,废话少说,记录一下今天初学的spider(甚至说不上是spider,I'm so vagetable [/认 ...

  3. 将本地代码初始化上传到gitlab仓库

    首先你已经安装了git. 1.在本地代码目录,鼠标右键Git Bash Here: 2.执行git命令,此命令会在当前目录下创建一个.git文件夹, git init 3.将项目的所有文件添加到仓库中 ...

  4. day42 Pyhton 并发编程05

    一.内容回顾 # 线程 # 正常的编程界: # 进程 # 计算机中最小的资源分配单位 # 数据隔离 # 进程可以独立存在 # 创建与销毁 还有切换 都慢 给操作系统压力大 # 线程 # 计算机中能被C ...

  5. ansible使用script模块在受控机上执行脚本(ansible2.9.5)

    一,ansible的script模块的用途 script 模块用来在远程主机上执行 ansible 管理主机上的脚本, 即:脚本一直存在于 ansible 管理主机本地, 不需要手动拷贝到远程主机后再 ...

  6. 通过jQuery来获取DropDownList的Text/Value属性值

    脚本代码: <script src="Scripts/jquery-1.4.1-vsdoc.js" type="text/javascript">& ...

  7. 4.QOpenGLWidget-对三角形进行纹理贴图、纹理叠加

    在上章3.QOpenGLWidget-通过着色器来渲染渐变三角形,我们为每个顶点添加颜色来增加图形的细节,从而创建出有趣的图像.但是,如果想让图形看起来更真实,我们就必须有足够多的顶点,从而指定足够多 ...

  8. Typora图片一键传——picgo使用兰空图床

    Typora picgo使用兰空图床 最近smms图床免费版的速度眼看着越来越慢,特别是到晚上,图片几乎是在原地打转,于是我想替换掉Typora中picgo默认使用的smms图床,网上的教程清一色地只 ...

  9. django—模板相关

    关于在html文件中使用模板语言时,一些符号的含义 {{}}:显示变量相关,在模板渲染的时候替换成值 {%%}:处理逻辑相关 django模板语言中,对于列表字典等容器类型,不能够通过[ 索引/键值 ...

  10. 想买保时捷的运维李先生学Java性能之 运行时数据区域

    前言 不知道自己不知道,不知道自己知道,知道自己不知道,知道自己知道,目前处于知道自己不知道这个阶段,很痛苦啊,干了4年了运维,是一个坎.越来越发觉想要走得远,还是得扎根底.   一.运行时数据区域 ...