SocketChannel可以创建连接TCP服务的客户端,用于为服务发送数据,SocketChannel的写操作和连接操作在非阻塞模式下不会发生阻塞,这篇文章里的客户端采用SocketChannel实现,利用线程池模拟多个客户端并发访问服务端的情景。服务端仍然采用ServerSocket来实现,主要用来看下阻塞模式下的服务端在并发访问时所做出的的处理。

一、使用SocketChannel实现一个客户端


private static ExecutorService ctp = Executors.newCachedThreadPool(); public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
ctp.submit(IOTest::client); //并发十个客户端连接过去
}
} public static void client() {
ByteBuffer buffer = ByteBuffer.allocate(1024); //定义缓冲区
SocketChannel socketChannel = null;
try {
socketChannel = SocketChannel.open(); //打开SocketChannel
socketChannel.configureBlocking(false); //设置为非阻塞模式
socketChannel.connect(new InetSocketAddress("127.0.0.1", 2333)); //连接服务
while (true) {
if(socketChannel.finishConnect()){ //这里的finishConnect是尝试连接,有可能返回false,因此使用死循环进行连接检查,确保连接已经正常建立。
System.out.println("客户端已连接到服务器");
int i = 0;
while (i < 5) {
TimeUnit.SECONDS.sleep(1); //隔一秒钟写一条
String info = "来自客户端的第" + (i++) + "条消息";
buffer.clear();
buffer.put(info.getBytes());
buffer.flip();
while (buffer.hasRemaining()) {
socketChannel.write(buffer); //给服务写消息
}
}
break;
}
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
} finally {
try {
if (socketChannel != null) {
System.out.println("客户端Channel关闭");
socketChannel.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}

上面会同时产生10个客户端去连接服务端

二、使用ServerSocket实现一个BIO的TCP服务


ServerSocket serverSocket = null;
int recvMsgSize = 0;
InputStream in = null;
try {
serverSocket = new ServerSocket(2333); //开一个监听2333端口的TCP服务
byte[] recvBuf = new byte[1024];
while (true) {
Socket clntSocket = serverSocket.accept(); //探听有没有新的客户端连接进来,没有就阻塞
SocketAddress clientAddress = clntSocket.getRemoteSocketAddress(); //通过跟服务连接上的客户端socket,拿到客户端地址
System.out.println("连接成功,处理客户端:" + clientAddress);
in = clntSocket.getInputStream(); //数据流
while ((recvMsgSize = in.read(recvBuf)) != -1) { //读取发送的数据,当客户端未断开连接,且不往服务端发数据的时候,说明一直处于准备读的状态,会一直阻塞下去,直到有数据写入(读就绪)
byte[] temp = new byte[recvMsgSize];
System.arraycopy(recvBuf, 0, temp, 0, recvMsgSize);
System.out.println("收到客户端" + clientAddress + "的消息内容:" + new String(temp)); //打印消息
}
System.out.println("-----------------------------------");
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (serverSocket != null) {
System.out.println("socket关闭!");
serverSocket.close();
}
if (in != null) {
System.out.println("stream连接关闭!");
in.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}

运行上面的代码,服务端打印如下:


连接成功,处理客户端:/127.0.0.1:54688
收到客户端/127.0.0.1:54688的消息内容:来自客户端的第0条消息
收到客户端/127.0.0.1:54688的消息内容:来自客户端的第1条消息
收到客户端/127.0.0.1:54688的消息内容:来自客户端的第2条消息
收到客户端/127.0.0.1:54688的消息内容:来自客户端的第3条消息
收到客户端/127.0.0.1:54688的消息内容:来自客户端的第4条消息
-----------------------------------
连接成功,处理客户端:/127.0.0.1:54680
收到客户端/127.0.0.1:54680的消息内容:来自客户端的第0条消息来自客户端的第1条消息来自客户端的第2条消息来自客户端的第3条消息来自客户端的第4条消息
-----------------------------------
连接成功,处理客户端:/127.0.0.1:54689
收到客户端/127.0.0.1:54689的消息内容:来自客户端的第0条消息来自客户端的第1条消息来自客户端的第2条消息来自客户端的第3条消息来自客户端的第4条消息
-----------------------------------
连接成功,处理客户端:/127.0.0.1:54682
收到客户端/127.0.0.1:54682的消息内容:来自客户端的第0条消息来自客户端的第1条消息来自客户端的第2条消息来自客户端的第3条消息来自客户端的第4条消息
-----------------------------------
连接成功,处理客户端:/127.0.0.1:54683
收到客户端/127.0.0.1:54683的消息内容:来自客户端的第0条消息来自客户端的第1条消息来自客户端的第2条消息来自客户端的第3条消息来自客户端的第4条消息
-----------------------------------
连接成功,处理客户端:/127.0.0.1:54684
收到客户端/127.0.0.1:54684的消息内容:来自客户端的第0条消息来自客户端的第1条消息来自客户端的第2条消息来自客户端的第3条消息来自客户端的第4条消息
-----------------------------------
连接成功,处理客户端:/127.0.0.1:54685
收到客户端/127.0.0.1:54685的消息内容:来自客户端的第0条消息来自客户端的第1条消息来自客户端的第2条消息来自客户端的第3条消息来自客户端的第4条消息
-----------------------------------
连接成功,处理客户端:/127.0.0.1:54681
收到客户端/127.0.0.1:54681的消息内容:来自客户端的第0条消息来自客户端的第1条消息来自客户端的第2条消息来自客户端的第3条消息来自客户端的第4条消息
-----------------------------------
连接成功,处理客户端:/127.0.0.1:54686
收到客户端/127.0.0.1:54686的消息内容:来自客户端的第0条消息来自客户端的第1条消息来自客户端的第2条消息来自客户端的第3条消息来自客户端的第4条消息
-----------------------------------
连接成功,处理客户端:/127.0.0.1:54687
收到客户端/127.0.0.1:54687的消息内容:来自客户端的第0条消息来自客户端的第1条消息来自客户端的第2条消息来自客户端的第3条消息来自客户端的第4条消息
-----------------------------------

可以看到,消息是按照顺序,一个一个连接进来,然后完成处理的,至于后面的消息为什么会被合并成一个,也是这个原因,因为阻塞,所以等第一个连接逐条输出完成后,第二个连接进来,这时很可能客户端的SocketChannel已经将十条消息全部写入channel,等第一个连接处理完成后,接到第二条消息时就已经是全部的消息了,因此一次性输出,后面的合并也是这个原因(主要客户端使用NIO实现,因此写和连接服务不会发生阻塞,因此在第次个请求服务端还在处理时,其余的客户端数据也在执行并写入通道,最终服务端处理完第一个连接,然后继续接收第二个连接时,数据便是完整的5条数据了)。

上面的服务端是一个典型的阻塞IO的服务,accept在没有连接进来时会发生阻塞,read在客户端连接没关闭,且不再写消息时,服务端的read将一直处于读等待状态并阻塞,直到收到新的消息转为读就绪才会继续往下执行(这就是上面例子里第一个进来的连接可以逐条输出的原因),完全串行化,过程如下图:

图1 

下面,来改造下服务端,让其处理能力更好一些,除了accept,下面的处理逻辑全部交给线程池处理:


while (true) {
Socket clntSocket = serverSocket.accept(); //探听有没有新的客户端连接进来,没有就阻塞
SocketAddress clientAddress = clntSocket.getRemoteSocketAddress(); //通过跟服务连接上的客户端socket,拿到客户端地址
System.out.println("连接成功,处理客户端:" + clientAddress); ctp.execute(() -> {
int recvMsgSize = 0;
InputStream in = null; //数据流
try {
in = clntSocket.getInputStream();
while ((recvMsgSize = in.read(recvBuf)) != -1) { //读取发送的数据,当客户端未断开连接,且不往服务端发数据的时候,说明一直处于准备读的状态,会一直阻塞下去,直到有数据写入(读就绪)
byte[] temp = new byte[recvMsgSize];
System.arraycopy(recvBuf, 0, temp, 0, recvMsgSize);
System.out.println("收到客户端" + clientAddress + "的消息内容:" + new String(temp)); //打印消息
}
System.out.println("-----------------------------------");
} catch (IOException e) {
e.printStackTrace();
}
}); }

运行结果:


连接成功,处理客户端:/127.0.0.1:55259
连接成功,处理客户端:/127.0.0.1:55265
连接成功,处理客户端:/127.0.0.1:55266
连接成功,处理客户端:/127.0.0.1:55257
连接成功,处理客户端:/127.0.0.1:55260
连接成功,处理客户端:/127.0.0.1:55258
连接成功,处理客户端:/127.0.0.1:55261
连接成功,处理客户端:/127.0.0.1:55262
连接成功,处理客户端:/127.0.0.1:55263
连接成功,处理客户端:/127.0.0.1:55264
收到客户端/127.0.0.1:55265的消息内容:来自客户端的第0条消息
收到客户端/127.0.0.1:55266的消息内容:来自客户端的第0条消息
收到客户端/127.0.0.1:55258的消息内容:来自客户端的第0条消息
收到客户端/127.0.0.1:55257的消息内容:来自客户端的第0条消息
收到客户端/127.0.0.1:55261的消息内容:来自客户端的第0条消息
收到客户端/127.0.0.1:55262的消息内容:来自客户端的第0条消息
收到客户端/127.0.0.1:55263的消息内容:来自客户端的第0条消息
收到客户端/127.0.0.1:55260的消息内容:来自客户端的第0条消息
收到客户端/127.0.0.1:55259的消息内容:来自客户端的第0条消息
收到客户端/127.0.0.1:55264的消息内容:来自客户端的第0条消息
收到客户端/127.0.0.1:55265的消息内容:来自客户端的第1条消息
收到客户端/127.0.0.1:55266的消息内容:来自客户端的第1条消息
收到客户端/127.0.0.1:55258的消息内容:来自客户端的第1条消息
收到客户端/127.0.0.1:55257的消息内容:来自客户端的第1条消息
收到客户端/127.0.0.1:55261的消息内容:来自客户端的第1条消息
收到客户端/127.0.0.1:55260的消息内容:来自客户端的第1条消息
收到客户端/127.0.0.1:55262的消息内容:来自客户端的第1条消息
收到客户端/127.0.0.1:55263的消息内容:来自客户端的第1条消息
收到客户端/127.0.0.1:55259的消息内容:来自客户端的第1条消息
收到客户端/127.0.0.1:55264的消息内容:来自客户端的第1条消息
收到客户端/127.0.0.1:55266的消息内容:来自客户端的第2条消息
收到客户端/127.0.0.1:55262的消息内容:来自客户端的第2条消息
收到客户端/127.0.0.1:55261的消息内容:来自客户端的第2条消息
收到客户端/127.0.0.1:55260的消息内容:来自客户端的第2条消息
收到客户端/127.0.0.1:55263的消息内容:来自客户端的第2条消息
收到客户端/127.0.0.1:55257的消息内容:来自客户端的第2条消息
收到客户端/127.0.0.1:55265的消息内容:来自客户端的第2条消息
收到客户端/127.0.0.1:55258的消息内容:来自客户端的第2条消息
收到客户端/127.0.0.1:55259的消息内容:来自客户端的第2条消息
收到客户端/127.0.0.1:55264的消息内容:来自客户端的第2条消息
收到客户端/127.0.0.1:55258的消息内容:来自客户端的第3条消息
收到客户端/127.0.0.1:55266的消息内容:来自客户端的第3条消息
收到客户端/127.0.0.1:55257的消息内容:来自客户端的第3条消息
收到客户端/127.0.0.1:55262的消息内容:来自客户端的第3条消息
收到客户端/127.0.0.1:55261的消息内容:来自客户端的第3条消息
收到客户端/127.0.0.1:55263的消息内容:来自客户端的第3条消息
收到客户端/127.0.0.1:55265的消息内容:来自客户端的第3条消息
收到客户端/127.0.0.1:55260的消息内容:来自客户端的第3条消息
收到客户端/127.0.0.1:55264的消息内容:来自客户端的第3条消息
收到客户端/127.0.0.1:55259的消息内容:来自客户端的第3条消息
收到客户端/127.0.0.1:55266的消息内容:来自客户端的第4条消息
收到客户端/127.0.0.1:55265的消息内容:来自客户端的第4条消息
-----------------------------------
收到客户端/127.0.0.1:55263的消息内容:来自客户端的第4条消息
收到客户端/127.0.0.1:55261的消息内容:来自客户端的第4条消息
-----------------------------------
收到客户端/127.0.0.1:55260的消息内容:来自客户端的第4条消息
-----------------------------------
收到客户端/127.0.0.1:55262的消息内容:来自客户端的第4条消息
-----------------------------------
收到客户端/127.0.0.1:55257的消息内容:来自客户端的第4条消息
-----------------------------------
-----------------------------------
收到客户端/127.0.0.1:55258的消息内容:来自客户端的第4条消息
-----------------------------------
-----------------------------------
收到客户端/127.0.0.1:55264的消息内容:来自客户端的第4条消息
-----------------------------------
收到客户端/127.0.0.1:55259的消息内容:来自客户端的第4条消息
-----------------------------------

消息被分开了,接收连接虽然仍然是串行,但实际的处理速度在多线程的帮助下已经比之前快很多了,流程如下图:

图2

三、BIO总结

综合看下来,传统的阻塞IO,按照图2的方式进行,虽然利用多线程避免了read等操作的阻塞对accept的影响,提高了处理效率,但想象下,如果现在存在高并发的情况,图2的模型如果不使用线程池,就会创建大量线程,会发生大量的线程上下文切换,影响整体效率,并且会影响新的线程,如果使用线程池,虽然某种程度上避免了线程的创建和上下文切换的量级,但是在大量并发的场景下,会发生排队,一旦发生排队,紧接着就会影响到accept。

Java NIO学习与记录(四): SocketChannel与BIO服务器的更多相关文章

  1. Java NIO学习与记录(八): Reactor两种多线程模型的实现

    Reactor两种多线程模型的实现 注:本篇文章例子基于上一篇进行:Java NIO学习与记录(七): Reactor单线程模型的实现 紧接着上篇Reactor单线程模型的例子来,假设Handler的 ...

  2. Java NIO 学习笔记(四)----文件通道和网络通道

    目录: Java NIO 学习笔记(一)----概述,Channel/Buffer Java NIO 学习笔记(二)----聚集和分散,通道到通道 Java NIO 学习笔记(三)----Select ...

  3. Java NIO学习与记录(六): NIO线程模型

    NIO线程模型 上一篇说的是基于操作系统的IO处理模型,那么这一篇来介绍下服务器端基于IO模型和自身线程的处理方式. 一.传统阻塞IO模型下的线程处理模式 这种处理模型是基于阻塞IO进行的,上一篇讲过 ...

  4. Java NIO学习与记录(五): 操作系统的I/O模型

    操作系统的I/O模型 在开始介绍NIO Reactor模式之前,先来介绍下操作系统的五种I/O模型,了解了这些模型,对理解java nio会有不小的帮助. 先来看下一个服务端处理一次网络请求的流程图: ...

  5. Java NIO学习与记录(二):FileChannel与Buffer用法与说明

    FileChannel与Buffer用法与说明 上一篇简单介绍了NIO,这一篇将介绍FileChannel结合Buffer的用法,主要介绍Buffer FileChannel的简单使用&Buf ...

  6. Java NIO学习与记录(一):初识NIO

    初识 工作中有些地方用到了netty,netty是一个NIO框架,对于NIO却不是那么熟悉,这个系列的文章是我在学习NIO时的一个记录,也期待自己可以更好的掌握NIO. 一.NIO是什么? 非阻塞式I ...

  7. Java NIO学习与记录(七): Reactor单线程模型的实现

    Reactor单线程模型的实现 一.Selector&Channel 写这个模型需要提前了解Selector以及Channel,之前记录过FileChannel,除此之外还有以下几种Chann ...

  8. Java NIO学习与记录(三): Scatter&Gather介绍及使用

     Scatter&Gather介绍及使用 上一篇知道了Buffer的工作机制,以及FileChannel的简单用法,这一篇介绍下 Scatter&Gather 1.Scatter(分散 ...

  9. Java NIO 学习笔记(三)----Selector

    目录: Java NIO 学习笔记(一)----概述,Channel/Buffer Java NIO 学习笔记(二)----聚集和分散,通道到通道 Java NIO 学习笔记(三)----Select ...

随机推荐

  1. Django和Ajax

    本文目录 一 什么是Ajax 二 基于jquery的Ajax实现 三 案例 四 文件上传 五 Ajax提交json格式数据 六 Django内置的serializers(把对象序列化成json字符串) ...

  2. Web挖掘

    Web挖掘 Web挖掘的目标是从Web的超链接.网页内容和使用日志中探寻有用的信息.依据Web挖掘任务,可以划分为三种主要类型:Web结构挖掘.Web内容挖掘和Web使用挖掘.Web结构挖掘简单的说就 ...

  3. Thread.sleep() 和 Thread.yield() 区别

    1. Thread.yield(): api中解释: 暂停当前正在执行的线程对象,并执行其他线程. 注意:这里的其他也包含当前线程,所以会出现以下结果. public class Test exten ...

  4. 6、Docker Image

    6.1 什么是image 文件和meta data的集合(root filesystem) 分层的,并且每一层都可以添加.改变.删除文件,成为一个新的image 不同的image可以共享相同的laye ...

  5. CAS实战の获取多用户信息

    先列出版本号: 服务端版本:cas server 4.0.0 客户端版本:cas client 3.3.3 cas server step1:先将primaryPrincipalResolver be ...

  6. linux 用户/用户组添加修改删除(ubuntu/centos)

    一.LINUX(UBUNTU/CENTOS)用户添加删除修改 1.建用户: adduser web                             //新建web用户 useradd web  ...

  7. 浅议Github的注册和使用

    Self-introduction:编者本人叫司明周,现就读于南通大学计算机学院网络工程142班.爱好数学和音乐,喜欢数学中的逻辑性和天马行空的思维 编程能力:可以跳过略过得过且过吗..好吧,面对现实 ...

  8. day14(带参装饰器,迭代器,生成器,枚举对象)

    一,复习 ''' 函数的嵌套定义:在函数内部定义另一个函数 闭包:被嵌套的函数 -- 1.外层通过形参给内层函数传参 -- 2.验证执行 开放封闭原则: 功能可以拓展,但源代码与调用方式都不可以改变 ...

  9. HTML5、CSS3与响应式Web设计入门(2)

    HTML5的宽泛含义开拓了Web开发的视野,增加了开发方案的多样性,同时也带给很多Web开发者不小的困惑,就是HTML5在涉及到Web某个应用领 域的开发时,到底代表了什么?本篇文章的目的就在于跟大伙 ...

  10. 基于Extjs的web表单设计器 第二节——表单控件设计

    这一节介绍表单设计器的常用控件的设计. 在前面两章节的附图中我已经给出了表单控件的两大分类:区域控件.常用控件.这里对每个分类以及分类所包含的控件的作用进行一一的介绍,因为它们很重要,是表单设计器的基 ...