先启服务器,后启动客户端,服务器从客户端读数据read,客户端向服务器端写数据write
Channel:通道,与stream不同,Channel双向数通信:java.nio.channels
ServerSocketChannel SocketChannel,可实现TCP通信用,都可以开启非阻塞式的网络通信(FileChannel)
TCP协议:可靠传输协议(流式的数据传输),UDP:效率高的传输,无可靠性消耗---TCP,UDP都在传输层有公共协议,HTTP,TCP,POP3,FTP等,也可以自定义私有协议
ServerSocketChannel:代表服务器通道,无法直接new,构造方法是protect状态通过open()创建ServerSocketChannel对象。
用static去定义它的对象,可以开启阻塞模式和非阻塞模式(从父类调configblocking(),false为非阻塞模式,默认是阻塞)finishConnect()完成对服务器的连接。一般通过while循环来持续判断是否完成对服务器的连接。
SocketChannel和ServerSockeChannel一致,也是protect状态,无法直接new
SocketChannel代表客户端通道,也有两种模式,阻塞模式和非阻塞模式,也是通过open()获取对象。
通道通过缓冲区来读数据写数据。
非阻塞也有弊端,发送数据时它不管剩余空间多少也不阻塞一直发,所以导致有的数据发不出去。
非阻塞模式得自己控制,还得写代码循环控制,所以非阻塞的NIO比较复杂,自己写较多代码 。
客户端和服务器都可以双向接收数据都可以read和write,TCP协议会把得到的数据封装成固定大小的包来传输,所以TCP协议有可能把想传的数据打装包发出去,TCP协议没有对数据的封装设置边界,所以需要处理。
粘包:从客户端定义完数据内容和长度,write完数据向服务器端发送后,服务端初始化发送来的数据为1kb,通过判断以回车还行结尾等条件循环出真正传来数据的长度,然后定义出与传来数据一致长度的缓冲区,然后直接读取从客户端传来的数据,从而不浪费空间,而且数据全部读取。也可以私有协议来控制边界符!!按照规定的方式发数据,对面按照规定方式接收数据。
本次例子的私有协议的:约定双方发送数据的方式--最经典做法:头信息发长度,体信息发内容。
粘包问题的产生:TCP的底层是一个包一个包发出去,而且没有定长,无法判断数据边。
Selector:单一线程处理多个客户端,选择器,它是个抽象类,不能直接用,java.nio.Channel
客户端的请求在选择器上注册,记录请求谁先工作谁后工作。
具有selectionKey的可以证明被注册的请求,selectionkey是被注册的证据/凭据。
select():选择一组键,其相应的通道已被I/O操作准备就绪
判断哪个通道准备就绪了,select()方法判断有几个准备就绪了返回int,Selector可以管理多个channel
selectedKeys():先调用select再调selectedKeys,用selectedKeys选择出已就绪的通道。返回此选择器已选的键集。
选择器有三个键集:相当于一个数组
1.已注册(sk1,sk2,sk3),找到所有已注册的通道事件
2.已选择(sk1),正在处理的通道事件
3.待删除(),删除处理完的sk用it.remove();从已选择里删
chanle()取消通道事件,复制一份到待删除阶段,不会真正被移除
在selector大喊一声之前,会去待删除阶段检查是否有已经完成的sk,如果有会干掉待删除阶段的sk(此键集只处理之后的键)。
看一个NIO Server例子:
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 NIOServerDemo1 {// NIO的服务器程序,一个服务器对应多个客户端,客户端只向服务器传输
public static void main(String[] args) throws IOException {
Selector selc = Selector.open();// 创建一个选择器,选择器一般来说全局只有一个就够
ServerSocketChannel ssc = ServerSocketChannel.open();// 创建服务器对象
ssc.socket().bind(new InetSocketAddress(9999));// 服务器指定监听端口
ssc.configureBlocking(false);// 非阻塞模式
ssc.register(selc, SelectionKey.OP_ACCEPT);// 注册accept操作,请求申请注册
while (true) {
try {
// 开始循环进行select操作,处理就绪的键sk
// 执行selc操作,相当于大喊一声儿,注册在我身上的selectionkey们,哪一个对应的通道已经就绪了当初注册的事件
int seleCount = selc.select();
// 如果选择出的sk多余0个,表明有需要被处理的通道
if (seleCount > 0) {
// 选择出已经就绪的通道,可能是复数
Set<SelectionKey> set = selc.selectedKeys();
Iterator<SelectionKey> it = set.iterator();// 对set的便利用iterator
while (it.hasNext()) {
SelectionKey sk = it.next();// 遍历出每一个就绪的sk
// 根据sk注册的不同,分别处理
if (sk.isAcceptable()) {// 如果是ACCEPT操作
ServerSocketChannel sscx = (ServerSocketChannel) sk
.channel();// 获取sk对应的channel
SocketChannel sc = sscx.accept();// 接收连接,得到sc
sc.configureBlocking(false);// 开启sc的非阻塞模式
sc.register(selc, SelectionKey.OP_READ);// 将sc注册到selc上,关注READ方法。
} else if (sk.isConnectable()) {
} else if (sk.isReadable()) {// 如果是一个Read操作
SocketChannel sc = (SocketChannel) sk.channel();// 获取sk对应的channel通道
ByteBuffer temp = ByteBuffer.allocate(1);// 获取头信息,获知体数据的长度
String head = "";
while (!head.endsWith("\r\n")) {// 找指定后缀
sc.read(temp);
head += new String(temp.array());
temp.clear();
}
int len = Integer.parseInt(
head.substring(0, head.length() - 2));
ByteBuffer buf = ByteBuffer.allocate(len);// 创建缓冲区,接收数据
while (buf.hasRemaining()) {
sc.read(buf);// 读取缓冲区数据
}
String msg = new String(buf.array(),"utf-8");
System.out.println("服务器收到了客户端["
+ sc.socket().getInetAddress().getHostAddress()
+ "]发来的数据:" + msg);
} else if (sk.isWritable()) {// 如果是Write操作
} else {// 其它就报错
throw new RuntimeException("NIO");
}
it.remove();// 删除已处理过后的键,如果不删除会出现可能被重复处理的情况
}
}
} catch (Exception e) {
continue;
}}}}
再看另外一个NIO 例子:
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 NIOServerDemo2 {// NIO的服务器程序,一个服务器对应多个客户端,收一次数据,发一次数据
public static void main(String[] args) throws IOException {
Selector selc = Selector.open();// 创建一个选择器,选择器一般来说全局只有一个就够
ServerSocketChannel ssc = ServerSocketChannel.open();// 创建服务器对象
ssc.socket().bind(new InetSocketAddress(9999));// 服务器指定监听端口
ssc.configureBlocking(false);// 非阻塞模式
ssc.register(selc, SelectionKey.OP_ACCEPT);// 注册accept操作,请求申请注册
while (true) {
try {
// 开始循环进行select操作,处理就绪的键sk
// 执行selc操作,相当于大喊一声儿,注册在我身上的selectionkey们,哪一个对应的通道已经就绪了当初注册的事件
int seleCount = selc.select();
// 如果选择出的sk多余0个,表明有需要被处理的通道
if (seleCount > 0) {
// 选择出已经就绪的通道,可能是复数
Set<SelectionKey> set = selc.selectedKeys();
Iterator<SelectionKey> it = set.iterator();// 对set的便利用iterator
while (it.hasNext()) {
SelectionKey sk = it.next();// 遍历出每一个就绪的sk
// 根据sk注册的不同,分别处理
if (sk.isAcceptable()) {// 如果是ACCEPT操作
ServerSocketChannel sscx = (ServerSocketChannel) sk.channel();// 获取sk对应的channel
SocketChannel sc = sscx.accept();// 接收连接,得到sc
sc.configureBlocking(false);// 开启sc的非阻塞模式
sc.register(selc, SelectionKey.OP_READ | SelectionKey.OP_WRITE);// 将sc注册到selc上,关注READ方法。
} else if (sk.isConnectable()) {
} else if (sk.isReadable()) {// 如果是一个Read操作
SocketChannel sc = (SocketChannel) sk.channel();// 获取sk对应的channel通道
ByteBuffer temp = ByteBuffer.allocate(1);// 获取头信息,获知体数据的长度
String head = "";
while (!head.endsWith("\r\n")) {// 找指定后缀
sc.read(temp);
head += new String(temp.array());
temp.clear();
} }
int len = Integer.parseInt(
head.substring(0, head.length() - 2));
ByteBuffer buf = ByteBuffer.allocate(len);// 创建缓冲区,接收数据
while (buf.hasRemaining()) {
sc.read(buf);// 读取缓冲区数据
} }
String msg = new String(buf.array(),"utf-8");
System.out.println("服务器收到了客户端["+ sc.socket().getInetAddress().getHostAddress()+ "]发来的数据:" + msg);
} else if (sk.isWritable()) {// 如果是Write操作
SocketChannel scx = (SocketChannel)sk.channel();//获取通道
String str = "你好,我是服务器,哈哈哈哈!";//待发送的数据
String sentStr = str.getBytes("utf-8").length + "\r\n" +str;//处理协议
ByteBuffer buf = ByteBuffer.wrap(sentStr.getBytes("utf-8"));//发送数据
while(buf.hasRemaining()) {
scx.write(buf);
}
scx.register(selc, sk.interestOps() & ~SelectionKey.OP_WRITE);//取消WRITE注册
} else {// 其它就报错
throw new RuntimeException("NIO");
}
it.remove();// 删除已处理过后的键,如果不删除会出现可能被重复处理的情况
}
}
} catch (Exception e) {
continue;
}}}}