第一章:手动搭建I/O网络通信框架1:Socket和ServerSocket入门实战,实现单聊

第三章:手动搭建I/O网络通信框架3:NIO编程模型,升级改造聊天室

第四章:手动搭建I/O网络通信框架4:AIO编程模型,聊天室终极改造

  在第一章中运用Socket和ServerSocket简单的实现了网络通信。这一章,利用BIO编程模型进行升级改造,实现群聊聊天室。

  所谓BIO,就是Block IO,阻塞式的IO。这个阻塞主要发生在:ServerSocket接收请求时(accept()方法)、InputStream、OutputStream(输入输出流的读和写)都是阻塞的。这个可以在下面代码的调试中发现,比如在客户端接收服务器消息的输入流处打上断点,除非服务器发来消息,不然断点是一直停在这个地方的。也就是说这个线程在这时间是被阻塞的。

  

  如图:当一个客户端请求进来时,接收器会为这个客户端分配一个工作线程,这个工作线程专职处理客户端的操作。在上一章中,服务器接收到客户端请求后就跑去专门服务这个客户端了,所以当其他请求进来时,是处理不到的。

  看到这个图,很容易就会想到线程池,BIO是一个相对简单的模型,实现它的关键之处也在于线程池。

  在上代码之前,先大概说清楚每个类的作用,以免弄混淆。更详细的说明,都写在注释当中。

  服务器端:

  ChatServer:这个类的作用就像图中的Acceptor。它有两个比较关键的全局变量,一个就是存储在线用户信息的Map,一个就是线程池。这个类会监听端口,接收客户端的请求,然后为客户端分配工作线程。还会提供一些常用的工具方法给每个工作线程调用,比如:发送消息、添加在线用户等。我之前简单用过Netty和WebSocket,这个类看上去就已经和这些框架有点相似了。学习IO编程模型也是为了接下来深入学习Netty做准备。

  ChatHandler:这个类就是工作线程的类。在这个项目中,它的工作很简单:把接收到的消息转发给其他客户端,当然还有一些小功能,比如添加\移除在线用户。

  客户端:

  相较于服务器,客户端的改动较小,主要是把等待用户输入信息这个功能分到其他线程做,不然这个功能会一直阻塞主线程,导致无法接收其他客户端的消息。

  ChatClient:客户端启动类,也就是主线程,会通过Socket和服务器连接。也提供了两个工具方法:发送消息和接收消息。

  UserInputHandler:专门负责等待用户输入信息的线程,一旦有信息键入,就马上发送给服务器。

  首先创建两个包区分一下客户端和服务器,client和server

  服务器端ChatServer:

public class ChatServer {
private int DEFAULT_PORT = ;
/**
* 创建一个Map存储在线用户的信息。这个map可以统计在线用户、针对这些用户可以转发其他用户发送的消息
* 因为会有多个线程操作这个map,所以为了安全起见用ConcurrentHashMap
* 在这里key就是客户端的端口号,但在实际中肯定不会用端口号区分用户,如果是web的话一般用session。
* value是IO的Writer,用以存储客户端发送的消息
*/
private Map<Integer, Writer> map=new ConcurrentHashMap<>();
/**
* 创建线程池,线程上限为10个,如果第11个客户端请求进来,服务器会接收但是不会去分配线程处理它。
* 前10个客户端的聊天记录,它看不见。当有一个客户端下线时,这第11个客户端就会被分配线程,服务器显示在线
* 大家可以把10再设置小一点,测试看看
* */
private ExecutorService executorService= Executors.newFixedThreadPool();
//客户端连接时往map添加客户端
public void addClient(Socket socket) throws IOException {
if (socket != null) {
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream())
);
map.put(socket.getPort(), writer);
System.out.println("Client["+socket.getPort()+"]:Online");
}
} //断开连接时map里移除客户端
public void removeClient(Socket socket) throws Exception {
if (socket != null) {
if (map.containsKey(socket.getPort())) {
map.get(socket.getPort()).close();
map.remove(socket.getPort());
}
System.out.println("Client[" + socket.getPort() + "]Offline");
}
} //转发客户端消息,这个方法就是把消息发送给在线的其他的所有客户端
public void sendMessage(Socket socket, String msg) throws IOException {
//遍历在线客户端
for (Integer port : map.keySet()) {
//发送给在线的其他客户端
if (port != socket.getPort()) {
Writer writer = map.get(port);
writer.write(msg);
writer.flush();
}
}
} //接收客户端请求,并分配Handler去处理请求
public void start() {
try (ServerSocket serverSocket = new ServerSocket(DEFAULT_PORT)) {
System.out.println("Server Start,The Port is:"+DEFAULT_PORT);
while (true){
//等待客户端连接
Socket socket=serverSocket.accept();
//为客户端分配一个ChatHandler线程
executorService.execute(new ChatHandler(this,socket));
}
} catch (IOException e) {
e.printStackTrace();
}
} public static void main(String[] args) {
ChatServer server=new ChatServer();
server.start();
}
}

  服务器端ChatHandler:

public class ChatHandler implements Runnable {
private ChatServer server;
private Socket socket; //构造函数,ChatServer通过这个分配Handler线程
public ChatHandler(ChatServer server, Socket socket) {
this.server = server;
this.socket = socket;
} @Override
public void run() {
try {
//往map里添加这个客户端
server.addClient(socket);
//读取这个客户端发送的消息
BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream())
);
String msg = null;
while ((msg = reader.readLine()) != null) {
//这样拼接是为了让其他客户端也能看清是谁发送的消息
String sendmsg = "Client[" + socket.getPort() + "]:" + msg;
//服务器打印这个消息
System.out.println(sendmsg);
//将收到的消息转发给其他在线客户端
server.sendMessage(socket, sendmsg + "\n");
if (msg.equals("quit")) {
break;
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//如果用户退出或者发生异常,就在map中移除该客户端
try {
server.removeClient(socket);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

  客户端ChatClient:

public class ChatClient {
private BufferedReader reader;
private BufferedWriter writer;
private Socket socket;
//发送消息给服务器
public void sendToServer(String msg) throws IOException {
//发送之前,判断socket的输出流是否关闭
if (!socket.isOutputShutdown()) {
//如果没有关闭就把用户键入的消息放到writer里面
writer.write(msg + "\n");
writer.flush();
}
}
//从服务器接收消息
public String receive() throws IOException {
String msg = null;
//判断socket的输入流是否关闭
if (!socket.isInputShutdown()) {
//没有关闭的话就可以通过reader读取服务器发送来的消息。注意:如果没有读取到消息线程会阻塞在这里
msg = reader.readLine();
}
return msg;
} public void start() {
//和服务创建连接
try {
socket = new Socket("127.0.0.1", 8888);
reader=new BufferedReader(
new InputStreamReader(socket.getInputStream())
);
writer=new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream())
);
//新建一个线程去监听用户输入的消息
new Thread(new UserInputHandler(this)).start();
/**
* 不停的读取服务器转发的其他客户端的信息
* 记录一下之前踩过的小坑:
* 这里一定要创建一个msg接收信息,如果直接用receive()方法判断和输出receive()的话会造成有的消息不会显示
* 因为receive()获取时,在返回之前是阻塞的,一旦接收到消息才会返回,也就是while这里是阻塞的,一旦有消息就会进入到while里面
* 这时候如果输出的是receive(),那么上次获取的信息就会丢失,然后阻塞在System.out.println
* */
String msg=null;
while ((msg=receive())!=null){
System.out.println(msg);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
if(writer!=null){
writer.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
} public static void main(String[] args) {
new ChatClient().start();
}
}

  客户端UserInputHandler:

public class UserInputHandler implements Runnable {
private ChatClient client; public UserInputHandler(ChatClient client) {
this.client = client;
} @Override
public void run() {
try {
//接收用户输入的消息
BufferedReader reader = new BufferedReader(
new InputStreamReader(System.in)
);
//不停的获取reader中的System.in,实现了等待用户输入的效果
while (true) {
String input = reader.readLine();
//向服务器发送消息
client.sendToServer(input);
if (input.equals("quit"))
break;
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

  运行测试:

  通过打开终端,通过javac编译。如果大家是在IDEA上编码的话可能会报编码错误,在javac后面加上-encoding utf-8再接java文件就好了。

  编译后运行,通过java运行时,又遇到了一个坑。会报找不到主类的错误,原来是因为加上两个包,要在class文件名前面加上包名。比如当前在src目录,下面有client和server两个包,要这么运行:java client.XXXX。可我之前明明在client文件夹下运行的java,也是不行,不知道为什么。

  接着测试:

  1.首先在一个终端里运行ChatServer,打开服务器

  2.在第二个终端里打开ChatClient,暂且叫A,此时服务器的终端显示:

  3.类似的,在第三个终端里打开ChatClient,暂且叫B,此时服务器显示:

  4.A中输入hi,除了服务器会打印hi外,B中也会显示,图片中的端口号和前面的不一样,是因为中间出了点小问题,前三张截图和后面的不是同时运行的。实际中同一个客户端会显示一样的端口号:

  5.当客户端输入quit时就会断开连接,最后,服务器的显示为:

手动搭建I/O网络通信框架2:Socket和ServerSocket入门实战,实现单聊的更多相关文章

  1. 手动搭建I/O网络通信框架1:Socket和ServerSocket入门实战,实现单聊

    资料:慕课网 第二章:手动搭建I/O网络通信框架2:Socket和ServerSocket入门实战,实现单聊 这个基础项目会作为BIO.NIO.AIO的一个前提,后面会有数篇博客会基于这个小项目利用B ...

  2. 手动搭建I/O网络通信框架3:NIO编程模型,升级改造聊天室

    第一章:手动搭建I/O网络通信框架1:Socket和ServerSocket入门实战,实现单聊 第二章:手动搭建I/O网络通信框架2:BIO编程模型实现群聊 在第二章中用BIO编程模型,简单的实现了一 ...

  3. 手动搭建I/O网络通信框架4:AIO编程模型,聊天室终极改造

    第一章:手动搭建I/O网络通信框架1:Socket和ServerSocket入门实战,实现单聊 第二章:手动搭建I/O网络通信框架2:BIO编程模型实现群聊 第三章:手动搭建I/O网络通信框架3:NI ...

  4. SSM框架手动搭建

    SSM框架手动搭建 创建web项目 IDEA创建Maven项目 [File]-->[new]-->[project..] 将项目变为web项目 [File]-->[Project S ...

  5. ASP.NET Web API 2系列(一):初识Web API及手动搭建基本框架

    1.导言 随着Web技术的发展,现在各种框架,前端的,后端的,数不胜数.全栈工程师的压力越来越大. PC端,pad端,移动端App(安卓/IOS)的发展,使得前后端一体的开发模式十分笨重.因此,前后端 ...

  6. 手动搭建简易web框架与django框架简介

    目录 纯手写简易web框架 基于wsgiref模块 动静态网页 简单了解jinja2模块 框架请求流程 python主流web框架 django框架 简介 应用app 命令操作django pycha ...

  7. vue新手入门之使用vue框架搭建用户登录注册案例,手动搭建webpack+Vue项目(附源码,图文详解,亲测有效)

    前言 本篇随笔主要写了手动搭建一个webpack+Vue项目,掌握相关loader的安装与使用,包括css-loader.style-loader.vue-loader.url-loader.sass ...

  8. 手动从0搭建ABP框架-ABP官方完整解决方案和手动搭建简化解决方案实践

      本文主要讲解了如何把ABP官方的在线生成解决方案运行起来,并说明了解决方案中项目间的依赖关系.然后手动实践了如何从0搭建了一个简化的解决方案.ABP官方的在线生成解决方案源码下载参考[3],手动搭 ...

  9. 混合式app ionic2.x 手动搭建开发环境教程分享(nodejs,jdk,ant,androidsdk)

    1.ionic简介 为什么选用ionic: 彻底开源且免费 性能优异 基于红的发紫的AngularJs 漂亮的UI 强大的命令行(基于更热门的nodejs) 开发团队非常活跃 ngCordova,将主 ...

随机推荐

  1. 配置ubunto 流量使用限制 python 实现简单 http server

    很多ubunto 都有流量限制,使用流量.如每天使用200M ,超过了就要提示信息 原理,在本机 开一个 http 服务, 显示错误信息,哪流量使用完以后,使用 iptables 将 流量转发到 本机 ...

  2. Python模块一

    logging模块 我们来说一下这个logging模块,这个模块的功能是记录我们软件的各种状态,你们现在和我一起找到红蜘蛛的那个图标,然后右键找一找是不是有个错误日志.其实每个软件都是有错误日志的,开 ...

  3. Vue2.0 【第二季】第9节 Component 标签

    目录 Vue2.0 [第二季]第9节 Component 标签 第9节 Component 标签 1.我们先在构造器外部定义三个不同的组件,分别是componentA,componentB和compo ...

  4. 03 Uipath调用VBA脚本,处理excel文档格式

    前言: 在平时我们的工作中,经常需要使用Uipath自动的导入大量数据到Excel表格中,但是却发现,数据导入到Excel之后,格式却是很乱,基本不能看,就像下图: ​ 而Uipath对Excel的操 ...

  5. js 模拟滚动条

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...

  6. MVC超链接调用控制器内的方法

    <a href="hello/Layout?name=Tom"><h1><span>Hello</span>World</h1 ...

  7. 标题 发布状态 评论数 阅读数 操作 操作 CNN目标检测系列算法发展脉络简析——学习笔记(三):Fast R-CNN

    最近两周忙着上网课.投简历,博客没什么时间写,姑且把之前做的笔记放上来把... 下面是我之前看论文时记的笔记,之间copy上来了,内容是Fast R-CNN的,以后如果抽不出时间写博客,就放笔记上来( ...

  8. Linux常用命令 - top命令详解(重点)

    21篇测试必备的Linux常用命令,每天敲一篇,每次敲三遍,每月一循环,全都可记住!! https://www.cnblogs.com/poloyy/category/1672457.html top ...

  9. Multi-batch TMT reveals false positives, batch effects and missing values(解读人:胡丹丹)

    文献名:Multi-batch TMT reveals false positives, batch effects and missing values (多批次TMT定量方法中对假阳性率,批次效应 ...

  10. Vue + element从零打造一个H5页面可视化编辑器——pl-drag-template

    pl-drag-template Github地址:https://github.com/livelyPeng/pl-drag-template 前言 想必你一定使用过易企秀或百度H5等微场景生成工具 ...