上一章《5.IO流》 ,我们的主题是IO,讨论的是数据在内存和硬盘之间的传输。这一章,我们讨论数据在网络之间的传输。
网络编程。
网络编程基本要素
网络编程的基本要素有三个。
IP
端口
协议
我们分别讨论。
IP
什么是IP
要想让网络中的计算机能够互相通信,必须为每台计算机指定一个标识号,通过这个标识号来指定要接收数据的计算机和识别发送数据的计算机,而IP地址就是这个标识号。也就是设备的标识。
IP地址分为两大类
IPv4
IPv6
IPv4是我们最常见的一类IP地址,比如我们设置路由器时候的"192.168.0.1",其特点是用32位(4字节)来表示地址,比如:“11000000 10101000 00000000 00000001”。这也不是我们常见的"192.168.0.1"的形式啊。再将其表示成十进制的形式,中间使用符号"."分隔不同的字节。
IPv6,这种我们或许不常见。IPv6诞生的原因是,随着互联网的蓬勃发展,IP地址的需求量愈来愈大,IPv4不够用了,所以有了IPv6。IPv6采用128位地址长度,每16个字节一组,分成8组十六进制数。例如:ABCD:EF01:2345:6789:ABCD:EF01:2345:6789。
我们可以通过命令的方式查看我们本机的IP地址。
不同的操作系统略有区别。
在Windows是ipconfig
在Mac和Linux是ifconfig
(关于Linux中的ifconfig,可以参考我们在《Linux操作系统使用入门:2.命令》 中关于"网络"部分的讨论。)
回送地址
大家或许都听过这个特殊的IP地址,127.0.0.1,这是回送地址,可以代表本机地址。此外,大家或许还听过localhost。
那么localhost和127.0.0.1是什么关系呢?
localhost就是127.0.0.1,127.0.0.1就是localhost?
不
localhost是个域名,不是地址,它可以被配置为任意的IP地址,只不过通常情况下都指向127.0.0.1(ipv4)和::1(ipv6)。
InetAddress
上述,我们提到了,可以通过命令的方式获取机器的IP地址。除了这种方法,Java也直接给我们提供了工具类来获取。
java.net.InetAddress
常用方法:
方法名
说明
static InetAddress getByName(String host)
确定主机名称的IP地址。主机名称可以是机器名称,也可以是IP地址。
String getHostName()
获取此IP地址的主机名。
String getHostAddress()
返回文本显示中的IP地址字符串。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.kakawanyifan;import java.net.InetAddress;import java.net.UnknownHostException;public class InetAddressDemo { public static void main (String[] args) throws UnknownHostException { InetAddress address = InetAddress.getByName("localhost" ); String name = address.getHostName(); String ip = address.getHostAddress(); System.out.println("主机名:" + name); System.out.println("IP地址:" + ip); } }
运行结果:
1 2 主机名:localhost IP地址:127.0.0.1
端口
网络编程的基本要素,第二个,端口。
通过上文的讨论,我们知道IP地址是机器的唯一表示。但是机器上有很多的应用程序,怎么才能被我们希望的那个应用程序接收到呢?
通过端口号,端口号是应用程序在设备上的唯一标识。
端口号,用两个字节表示的整数,它的取值范围是[ 0 , 65535 ] [0,65535] [ 0 , 6 5 5 3 5 ] 。如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败。
题外话。65535?怎么这么熟悉?这也是基本数据类型中的字符型的取值范围。 在《1.基础语法》 ,我们讨论过。 字符类型,char,2字节,取值范围[ 0 , 65535 ] [0,65535] [ 0 , 6 5 5 3 5 ] 。
协议
网络编程的基本要素,第三个,协议。
现在我们知道了,用IP地址来标识设备,用端口号来标识设备上的应用程序。但还有一件事情没有说清楚,怎么沟通?大家得定一个规则。
就像打电话一样,别拿起电话就说话,先说声喂喂,确认是否接通。另外,别说方言,请讲普通话。
这既是通信协议,是位于同一个网络中的计算机在进行连接和通信时需要遵守一定的规则。这些协议对数据的传输格式、传输速率、传输步骤等都做了统一规定,通信双方必须同时遵守才能完成数据交换。
在这里,我们讨论UDP协议和TCP协议
UDP协议,用户数据报协议(User Datagram Protocol)。
UDP是无连接通信协议。无连接的含义是在数据传输时,数据的发送端和接收端不建立逻辑连接,简单来说,当一台计算机向另外一台计算机发送数据时,发送端不会确认接收端是否存在,同样接收端在收到数据时,也不会向发送端反馈是否收到数据。
由于使用UDP协议消耗资源小,通信效率高,所以通常都会用于音频、视频和普通数据的传输。例如视频会议通常采用UDP协议,因为这种情况即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。
但是在使用UDP协议传送数据时,由于UDP的面向无连接性,不能保证数据的完整性,因此在传输重要数据时不建议使用UDP协议,比如交易指令。我要买入100手,这个数据包丢了?那就是严重的生产事故。
TCP协议,传输控制协议(Transmission Control Protocol)。
TCP协议是面向连接的通信协议,即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。在TCP连接中必须要明确客户端与服务器端,由客户端向服务端发出连接请求,每次连接的创建都需要经过"三次握手"。
第一次握手,客户端向服务器端发出连接请求,等待服务器确认。
第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求。
第三次握手,客户端再次向服务器端发送确认信息,确认连接。
完成三次握手,连接建立后,客户端和服务器就可以开始进行数据传输了。由于这种面向连接的特性,TCP协议可以保证传输数据的安全,所以应用十分广泛。例如上传文件、下载文件、浏览网页
至此,关于网络编程的三个基本要素,我们都讨论好了。接下来,我们分别基于UDP协议和TCP协议,实现几个网络通信的例子。
UDP通信程序
相关类和方法
正如UDP的英文名,User Datagram Protocol。
在Java中,UDP通信主要依赖两个类,这两个类的名字都以Datagram开头了。
一个类是Socket类,java.net.DatagramSocket。
还有一个是数据包类,java.net.DatagramPacket。
发送数据的步骤
创建发送端的Socket对象:DatagramSocket的构造方法
创建DatagramPacket对象:DatagramPacket的构造方法
调用DatagramSocket对象的方法发送数据:DatagramSocket的发送方法
关闭发送端:DatagramSocket的关闭方法
接收数据的步骤
创建接收端的Socket对象:DatagramSocket的构造方法
创建一个数据包,用于接收数据:DatagramPacket的构造方法
调用DatagramSocket对象的方法接收数据:DatagramSocket的接收方法
解析数据包:DatagramPacket的解析方法
关闭接收端:DatagramSocket的关闭方法
我们分别看看上述步骤提到的方法。
DatagramSocket的构造方法
方法名
说明
DatagramSocket()
创建数据报套接字并将其绑定到本机地址上的任何可用端口
DatagramPacket(byte[] buf,int len,InetAddress add,int port)
创建数据包,发送长度为len的数据包到指定主机的指定端口
DatagramSocket的发送、接收和关闭方法
方法名
说明
void send(DatagramPacket p)
发送数据报包
void receive(DatagramPacket p)
从此套接字接受数据报包
void close()
关闭数据报套接字
DatagramPacket的构造方法
方法名
说明
DatagramPacket(byte[] buf, int len)
创建一个DatagramPacket用于接收长度为len的数据包
DatagramPacket的解析方法
方法名
说明
byte[] getData()
返回数据缓冲区
int getLength()
返回要发送的数据的长度或接收的数据的长度
例子
来看个例子
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package com.kakawanyifan;import java.io.IOException;import java.net.*;public class SendDemo { public static void main (String[] args) throws IOException { DatagramSocket ds = new DatagramSocket(); byte [] bys = "hello world" .getBytes(); DatagramPacket dp = new DatagramPacket(bys,bys.length, InetAddress.getByName("localhost" ),12345 ); ds.send(dp); ds.close(); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package com.kakawanyifan;import java.io.IOException;import java.net.DatagramPacket;import java.net.DatagramSocket;public class ReceiveDemo { public static void main (String[] args) throws IOException { DatagramSocket ds = new DatagramSocket(12345 ); while (true ) { byte [] bys = new byte [1024 ]; DatagramPacket dp = new DatagramPacket(bys, bys.length); ds.receive(dp); System.out.println("数据是:" + new String(dp.getData(), 0 , dp.getLength())); } } }
运行结果:
解释说明:
在ReceiveDemo中,因为接收端不知道发送端什么时候结束,所以采用了死循环,一直去接收数据。
注意!是不知道什么时候结束 ,因为ds.receive(dp)实际上是一个阻塞方法,会在等到收到数据包后,再执行后续的操作。但可能收到一个数据包,发送端还会继续发数据包,所以用死循环。
TCP通信程序
接下来,我们再来讨论TCP通信程序。
相关方法
与UDP不同的是,在TCP中有客户端和服务端的概念(毕竟有握手的过程了)。
和客户端相关的类是java.net.Socket。
构造方法
方法名
说明
Socket(InetAddress address,int port)
创建流套接字并将其连接到指定IP指定端口号
Socket(String host, int port)
创建流套接字并将其连接到指定主机上的指定端口号
和服务端相关的类是java.net.ServerSocket。
构造方法
方法名
说明
ServletSocket(int port)
创建绑定到指定端口的服务器套接字
接收方法
方法名
说明
Socket accept()
监听要连接到此的套接字并接受它
服务端只有接收方法?没有发送方法吗?服务端的接收返回,返回的是什么?就是一个Socket,所以用这个Socket再发送内容给客户端。
发送与接收方法
方法名
说明
OutputStream getOutputStream()
返回此套接字的输出流
InputStream getInputStream()
返回此套接字的输入流
现在问谁是发送,谁是接收?
我们在写哪一端的代码,就站在哪一端的角度看,out是发送,in是接收。
例子
举个例子
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package com.kakawanyifan;import java.io.IOException;import java.io.InputStream;import java.io.OutputStream;import java.net.Socket;public class ClientDemo { public static void main (String[] args) throws IOException { Socket s = new Socket("127.0.0.1" ,12345 ); OutputStream os = s.getOutputStream(); os.write("hello world" .getBytes()); InputStream is = s.getInputStream(); byte [] bys = new byte [1024 ]; int len = is.read(bys); String data = new String(bys, 0 , len); System.out.println(data); is.close(); os.close(); s.close(); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 package com.kakawanyifan;import java.io.IOException;import java.io.InputStream;import java.io.OutputStream;import java.net.ServerSocket;import java.net.Socket;public class ServerDemo { public static void main (String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(12345 ); while (true ) { Socket socket = serverSocket.accept(); InputStream is = socket.getInputStream(); byte [] bys = new byte [1024 ]; int len = is.read(bys); String data = new String(bys, 0 , len); System.out.println("服务端收到数据:" + data); OutputStream os = socket.getOutputStream(); os.write(("数据已经收到:内容是" + data).getBytes()); socket.close(); } } }
运行结果:
例子加强:上传文件
我们再把这一章的内容和上一章《5.IO流》 联合起来,做一个上传文件的例子。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 package com.kakawanyifan;import java.io.*;import java.net.ServerSocket;import java.net.Socket;public class ServerDemo { public static void main (String[] args) throws IOException, InterruptedException { ServerSocket serverSocket = new ServerSocket(12345 ); while (true ) { Socket socket = serverSocket.accept(); BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream())); BufferedWriter bw = new BufferedWriter(new FileWriter("server.txt" )); String line; while ((line=br.readLine())!=null ) { bw.write(line); bw.newLine(); bw.flush(); } br.close(); BufferedWriter bwServer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); bwServer.write("文件上传成功" ); bwServer.newLine(); bwServer.flush(); socket.close(); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package com.kakawanyifan;import java.io.*;import java.net.Socket;public class ClientDemo { public static void main (String[] args) throws IOException { Socket socket = new Socket("127.0.0.1" ,12345 ); BufferedReader br = new BufferedReader(new FileReader("射雕英雄传.txt" )); BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); String line; while ((line=br.readLine())!=null ) { bw.write(line); bw.newLine(); bw.flush(); } socket.shutdownOutput(); BufferedReader brClient = new BufferedReader(new InputStreamReader(socket.getInputStream())); String data = brClient.readLine(); System.out.println("服务器的反馈:" + data);、 br.close(); socket.close(); } }
BIO的缺陷
阻塞
什么是BIO?
刚刚我们讨论的就是BIO。
为什么是这个名字,B是什么含义?
B的含义是Blocking,阻塞。
在UDP的例子中,我们就已经解释阻塞了。不收到数据包,就不往下执行。
那么,为什么阻塞会是一个缺陷呢?
举例子说明。
比如,我们从键盘录入,模拟我们使用百度的情况,从键盘录入信息发送给百度。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package com.kakawanyifan;import java.io.IOException;import java.io.InputStream;import java.net.ServerSocket;import java.net.Socket;public class ServerDemo { public static void main (String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(12345 ); while (true ) { System.out.println("等待连接" ); Socket socket = serverSocket.accept(); System.out.println("连接成功" ); System.out.println("等待数据" ); InputStream is = socket.getInputStream(); byte [] bys = new byte [1024 ]; int len = is.read(bys); String data = new String(bys, 0 , len); System.out.println("收到数据:" + data); socket.close(); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.kakawanyifan;import java.io.IOException;import java.io.OutputStream;import java.net.Socket;import java.util.Scanner;public class ClientDemo { public static void main (String[] args) throws IOException { Socket socket = new Socket("127.0.0.1" ,12345 ); Scanner scanner = new Scanner(System.in); String text = scanner.next(); OutputStream os = socket.getOutputStream(); os.write(text.getBytes()); os.close(); socket.close(); } }
运行结果:
1 2 3 4 5 等待连接 连接成功 等待数据 收到数据:1 等待连接
我们解释一下上述的运行结果。
首先,我们启动服务端,然后服务端打印等待连接。
再启动我们的客户端,客户端会执行Socket socket = new Socket("127.0.0.1",12345),这时候服务端打印连接成功 等待数据。
然后我们再录入数据,服务端打印收到数据:1,再执行socket.close(),并进入下一轮循环等待连接。
整个过程似乎看起来没问题。
那么,假如两个客户端呢?我们把上述的客户端文件复制一份,重命名为ClientDemo2。
我们先执行ClientDemo、然后不录入数据。服务端的打印如下
继续执行ClientDemo2,并录入2。这时候看看服务端的打印
居然没有变?
再回到ClientDemo,录入1。服务端打印。
1 2 3 4 5 6 7 8 9 等待连接 连接成功 等待数据 收到数据:1 等待连接 连接成功 等待数据 收到数据:2 等待连接
为什么?
因为上述的Socket socket = serverSocket.accept()和InputStream is = socket.getInputStream()会阻塞。会一直等待,直到收到连接、收到输入,才会进行后面的操作。
这就是BIO,Blocking,阻塞的缺陷。
那么?这个怎么办呢?
多线程解决方案
有一款很经典的GBA游戏《牧场物语:矿石镇的伙伴们》,这个游戏大概内容就是玩家经营一个牧场,养鸡、养牛、种菜等,如果玩家忙不过来了,可以把某些活委托给小矮人。
实际上这个例子存在不合理之处,这个例子会让大家误以为多线程就是快,实际上多线程不一定快,这个我们会在《8.多线程 [2/2]》 做详细的讨论。
但在这里,有一个线程都已经阻塞了,用多线程就是会快。这个我们会在《7.多线程 [1/2]》 进行解释。
仿照牧场物语的例子,现在"玩家"专门复制接收socket,在接收到socket之后,马上把socket交给小矮人,剩下的事情交给小矮人去处理。
即一个线程负责一个socket。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.kakawanyifan;import java.io.IOException;import java.net.ServerSocket;import java.net.Socket;public class ServerDemo { public static void main (String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(12345 ); while (true ) { System.out.println("等待连接" ); Socket socket = serverSocket.accept(); System.out.println("连接成功" ); new Thread(new ServerThread(socket)).start(); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package com.kakawanyifan;import java.io.*;import java.net.Socket;public class ServerThread implements Runnable { private Socket socket; public ServerThread (Socket s) { this .socket = s; } @Override public void run () { try { System.out.println(Thread.currentThread().getName() + " 等待数据" ); InputStream is = socket.getInputStream(); byte [] bys = new byte [1024 ]; int len = is.read(bys); String data = new String(bys, 0 , len); System.out.println(Thread.currentThread().getName() + " 收到数据:" + data); socket.close(); } catch (IOException e) { e.printStackTrace(); } } }
运行结果:
1 2 3 4 5 6 7 8 9 等待连接 连接成功 等待连接 Thread-0 等待数据 连接成功 等待连接 Thread-1 等待数据 Thread-1 收到数据:2 Thread-0 收到数据:1
但,该方案有缺陷。
如果客户端只连接,但是不做读写操作,会造成资源浪费。
如果线程很多,会导致服务器线程太多,压力太大。
更好的办法,是NIO。
NIO
NIO的含义是"Non Blocking IO",没有阻塞的IO。
在上述,我们已经讨论过,多线程的方案不好,会造成资源浪费,而且如果线程多,服务器压力大。
那么就不要多线程,就一个线程。
可以如果一个线程呢?有会阻塞。
我只想用一个线程,还不会阻塞。
做不到。
但可以只用一个线程,看起来没有阻塞。
只用一个线程,解决并发。
NIO的设计思路
再来看看上述我们只用一个线程的过程,伪代码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class ServerDemo { public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(12345); while (true) { // 阻塞 Socket socket = serverSocket.accept(); // 阻塞 InputStream is = socket.getInputStream(); // 处理数据 System.out.println(is); // 释放资源 socket.close(); } } }
我们先处理socket.getInputStream()这个阻塞。
假设我们有一个方法,可以直接设置socket为不阻塞,比如,就是socket.set不阻塞()。
则有
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class ServerDemo { public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(12345); while (true) { // 阻塞 Socket socket = serverSocket.accept(); // 不阻塞 socket.set不阻塞() InputStream is = socket.getInputStream(); if(null != is){ // 处理数据 System.out.println(is); // 释放资源 socket.close(); } } } }
如果不阻塞的话,那么InputStream is = socket.getInputStream()就会直接执行。如果收到了数据就处理数据,没有收到数据,就继续执行,进入下一轮循环。
如果进入了下一轮循环,这时候客户端发送数据了,能收到吗?收不到。因为阻塞在serverSocket.accept()。
那就设置serverSocket也不阻塞,没有拿到新的socket连接对象,就直接continue。比如:serverSocket.set不阻塞()。
则有
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class ServerDemo { public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(12345); serverSocket.set不阻塞() while (true) { Socket socket = serverSocket.accept(); if(socket == null){ continue; } // 不阻塞 socket.set不阻塞() InputStream is = socket.getInputStream(); if(null != is){ // 处理数据 System.out.println(is); // 释放资源 socket.close(); } } } }
这样能解决问题吗?
不能,我们现在一直在获取新的socket对象,之前的客户端发的消息还是收不到。
那么,我们这样。每拿到一个socket对象,添加进一个list中。遍历那个list。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class ServerDemo { public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(12345); serverSocket.set不阻塞() List<Socket> sockstList = new ArrayList<>(); while (true) { // 阻塞 Socket socket = serverSocket.accept(); if(socket != null){ // 不阻塞 socket.set不阻塞() sockstList.add(socket) } for(Socket socket in sockstList){ // 不阻塞 InputStream is = socket.getInputStream(); if(null != is){ // 处理数据 System.out.println(is); // 释放资源 socket.close(); } } } } }
这就是NIO的设计思路,并没有解决阻塞,但是看起来没有阻塞。
只用了一个线程,解决了并发。
基于NIO的实现
设置不阻塞的两个方法是:
serverSocketChannel.configureBlocking(false);
socket.configureBlocking(false);
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 package com.kakawanyifan;import java.io.IOException;import java.net.InetSocketAddress;import java.net.SocketAddress;import java.nio.ByteBuffer;import java.nio.channels.ServerSocketChannel;import java.nio.channels.SocketChannel;import java.util.ArrayList;import java.util.Iterator;import java.util.List;public class NioServerDemo { public static void main (String[] args) throws IOException { ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); SocketAddress socketAddress = new InetSocketAddress(12345 ); serverSocketChannel.bind(socketAddress); serverSocketChannel.configureBlocking(false ); List<SocketChannel> socketChannelList = new ArrayList<>(); while (true ){ SocketChannel socket = serverSocketChannel.accept(); if (socket != null ){ System.out.println("连接成功" ); socket.configureBlocking(false ); socketChannelList.add(socket); } Iterator<SocketChannel> iterator = socketChannelList.iterator(); while (iterator.hasNext()){ SocketChannel socketChannel = iterator.next(); ByteBuffer byteBuffer = ByteBuffer.allocate(1024 ); int len = socketChannel.read(byteBuffer); if (len > 0 ){ System.out.println("收到数据:" + new String(byteBuffer.array(),0 ,len)); }else if (len == -1 ){ iterator.remove(); System.out.println("客户端断开连接" ); } } } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.kakawanyifan;import java.io.IOException;import java.io.OutputStream;import java.net.Socket;import java.util.Scanner;public class ClientDemo { public static void main (String[] args) throws IOException { Socket socket = new Socket("127.0.0.1" ,12345 ); Scanner scanner = new Scanner(System.in); String text = scanner.next(); OutputStream os = socket.getOutputStream(); os.write(text.getBytes()); os.close(); } }
运行结果:
1 2 3 4 5 6 7 8 9 连接成功 连接成功 收到数据:2 客户端断开连接 收到数据:1 客户端断开连接 连接成功 收到数据:3 客户端断开连接
注意NioServerDemo的代码,我们把for循环改成了用迭代器,因为for循环会报错,关于其报错原因,我们在《4.集合》 中有过讨论。
但是呢,上述方案还有改进空间,可能会有大量的无效遍历。
例如有10000个连接,其中只有1000个连接有写数据,但是由于其他9000个连接并没有断开,我们还是要每次轮询遍历一万次,其中有十分之九的遍历都是无效的。
解决方法是多路复用。
多路复用
NIO 有三大核心组件:
Channel(通道)
Buffer(缓冲区)
Selector(多路复用器)
channel类似于流,每个channel对应一个buffer缓冲区,buffer底层就是个数组。channel会注册到selector上,由selector根据channel读写事件的发生将其交由某个空闲的线程处理。NIO的Buffer和channel都是既可以读也可以写。
最重要的三个方法
Selector.open() //创建多路复用器
socketChannel.register(selector, SelectionKey.OP_READ) //将channel注册到多路复用器上
selector.select() //阻塞等待需要处理的事件发生
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 package com.kakawanyifan;import java.io.IOException;import java.net.InetSocketAddress;import java.nio.ByteBuffer;import java.nio.channels.SelectionKey;import java.nio.channels.Selector;import java.nio.channels.ServerSocketChannel;import java.nio.channels.SocketChannel;import java.util.Iterator;import java.util.Set;public class NioSelectorServer { public static void main (String[] args) throws IOException, InterruptedException { ServerSocketChannel serverSocket = ServerSocketChannel.open(); serverSocket.socket().bind(new InetSocketAddress(12345 )); serverSocket.configureBlocking(false ); Selector selector = Selector.open(); serverSocket.register(selector, SelectionKey.OP_ACCEPT); System.out.println("服务启动成功" ); while (true ) { selector.select(); Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = selectionKeys.iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); if (key.isAcceptable()) { ServerSocketChannel server = (ServerSocketChannel) key.channel(); SocketChannel socketChannel = server.accept(); socketChannel.configureBlocking(false ); socketChannel.register(selector, SelectionKey.OP_READ); System.out.println("客户端连接成功" ); } else if (key.isReadable()) { SocketChannel socketChannel = (SocketChannel) key.channel(); ByteBuffer byteBuffer = ByteBuffer.allocate(128 ); int len = socketChannel.read(byteBuffer); if (len > 0 ) { System.out.println("接收到消息:" + new String(byteBuffer.array(),0 ,len)); } else if (len == -1 ) { System.out.println("客户端断开连接" ); socketChannel.close(); } } iterator.remove(); } } } }
运行结果:
1 2 3 4 5 6 7 服务启动成功 客户端连接成功 客户端连接成功 接收到消息:2 客户端断开连接 接收到消息:1 客户端断开连接
解释说明:
NIO的多路复用方法,其本质是调用了操作系统的内核函数来创建Socket,获取到Socket的文件描述符,再创建一个Selector对象,对应操作系统的Epoll描述符,将获取到的Socket连接的文件描述符的事件绑定到Selector对应的Epoll文件描述符上,进行事件的异步通知,这样就实现了使用一条线程,并且不需要太多的无效的遍历,将事件处理交给了操作系统内核(操作系统中断程序实现),大大提高了效率。