socket: 套接字就是两台主机逻辑连接的端点。 我们先看一下HTTP以及TCP/IP
HTTP协议作为应用层协议主要解决如何包装数据, TCP/IP协议集处理传输层数据如何传输。而socket是通信的基石, 是支持TCP协议的网络通信的基本操作单元 。它具备了网络传输必须的5种信息, 之前讲过TCP连接前的TCB传输控制块中就有socket信息。
1. 连接使用的协议 2. 源IP 3. 源端口 4. 目的端口 5. 目的IP
我们网络的传输通过不同的IO模型构建的通道,性能肯定是不一样的。BIO NIO AIO
NIO: 同步非阻塞, 实现了一个线程处理多个请求, 中间有一个多路复用器, 所有的请求通道都注册在这个多路复用器上, 而只需要一个线程不断的轮询这些请求即可。并且每个通道即可读也可写。
具体的说: Channel, Buffer, selector
首先每个客服端对应一个Channel, 每个通道都哟一个Buffer, Buffer用来干嘛, 你可以把数据写到Buffer中, 也可以从中读取到数据,双向的数据内存块。除此之外所有的通道注册到selector中, selector对应一个线程, 这个线程可以通过selector轮询所有通道中的事件。 大致如此
一. 理解Buffer:
缓存区: 本质上是一个可以读写数据的内存块, Channel 提供从网络读取数据的渠道,但是读取或者写入必须经过buffer.
说一下Buffer的API, 注意点在, 读取之前我们需要操作flip()切换模式, 想要重复读必须操作remain(), 否则报错, 这个时候想要写入put(), 那么必须操作clear()切换回去, 但是此时数据并没有丢失, 等待被覆盖!
ByteBuffer buffer = ByteBuffer.allocate(8);
buffer.put("12345".getBytes()) ;
System.out.println(buffer.position()); // 这个是position为5
System.out.println(buffer.limit()); // 设个时候为8
buffer.flip() ; // 切换读之后 先设置limit=position, 在设置position=0
System.out.println(buffer.position());
System.out.println(buffer.limit());
for (int i = 0; i < buffer.limit(); i++) {
System.out.println(buffer.get());
}
buffer.clear() ; // 切换写之后设置limit=capacity, position=0
System.out.println(buffer.position());
System.out.println(buffer.limit());
System.out.println(buffer.get(1));
二. Channel通道
常用的实现类: FileChannel, DatagramChannel(用于UDP), ServerSocketChannel, socketChannel.
构建服务端通道
public static void main(String[] args) throws IOException, InterruptedException {
// 1. 打开服务通道
ServerSocketChannel channel = ServerSocketChannel.open();
// 2. 绑定端口
channel.bind(new InetSocketAddress(9999)) ;
// 3. 通道默认是阻塞的
channel.configureBlocking(true) ;
System.out.println("服务启动完成....");
while(true) {
// 4. 检查是否由客服端通道
SocketChannel clientChannal = channel.accept();
if (clientChannal == null) {
System.out.println("没有客户端连接...我去做别的事情");
Thread.sleep(2000); continue;
}
//5. 获取客户端传递过来的数据,并把数据放在byteBuffer这个缓冲区中
// 1. 创建Buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
//返回值:
// 正数: 表示本次读到的有效字节个数.
// 0 : 表示本次没有读到有效字节.
// -1 : 表示读到了末尾
int len = clientChannal.read(buffer);
System.out.println("客服端消息: " + new String(buffer.array(), 0, len, StandardCharsets.UTF_8));
// 因为通道是双向的我们还可以写回
clientChannal.write(ByteBuffer.wrap("随意即可".getBytes(StandardCharsets.UTF_8))) ;
clientChannel.close() ;
}
}
构建客服端通道
public static void main(String[] args) throws IOException {
// 1. 打开通道
SocketChannel clientChannel = SocketChannel.open();
// 2. 设置IP以及端口
clientChannel.connect(new InetSocketAddress("127.0.0.1", 9999)) ;
// 3. 写出数据
clientChannel.write(ByteBuffer.wrap("我来借点钱".getBytes(StandardCharsets.UTF_8)));
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = clientChannel.read(buffer);
System.out.println("服务端返回数据: " + new String(buffer.array(), 0, len));
clientChannel.close();
}
三. selector选择器
之前讲到, 选择器, 只有当通道发生读写事件后, 才会进行读写, 大大减少了系统开销, 不用维护多个线程, 避免多线程之间的上下文切换导致的开销。
Selector.open() 得到一个选择器
Selector.select() 监控所有通道, 当发生事件时, 把selectionKey放入集合,并返回事件数量
Selector.selectKeys(): // 返回存在的selectKeys集合
SelectKey :
定义了四种状态:
1. 接收连接就绪事件 OP_ACCEPT 表示服务端已监听到客服端连接请求, 服务端可以接收连接
2. 连接就绪事件 OP_CONNET 表示已经连接成功
3. 读就绪事件OP_READ 表示已经有了可读的数据
4. 写就绪事件OP_WRITE 表示已经向通道写了数据
public static void main(String[] args) throws IOException {
// 1. 打开一个服务端通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 2. 绑定对应的端口号
serverSocketChannel.bind(new InetSocketAddress(9999)) ;
// 3. 通道默认是阻塞的,需要设置为非阻塞
serverSocketChannel.configureBlocking(false) ;
// 4. 创建选择器
Selector selector = Selector.open();
// 5. 将服务端通道注册到选择器上,并指定注册监听的事件为OP_ACCEPT
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT) ;
System.out.println("服务端启动成功...");
while(true) {
// 6. 检查选择器是否有事件
int i = selector.select(2000);
if(i == 0) {
continue;
}
//7. 获取事件集合
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
//8. 判断事件是否是客户端连接事件SelectionKey.isAcceptable()
if(key.isAcceptable()){
//9. 得到客户端通道,并将通道注册到选择器上, 并指定监听事件为OP_READ
SocketChannel clientChannel = serverSocketChannel.accept() ;
System.out.println("客户端已连接......" + clientChannel);
//必须设置通道为非阻塞, 因为selector需要轮询监听每个通道的事件
clientChannel.configureBlocking(false) ;
clientChannel.register(selector, SelectionKey.OP_READ) ;
}
//10. 判断是否是客户端读就绪事件SelectionKey.isReadable()
if(key.isReadable()) {
//11.得到客户端通道,读取数据到缓冲区
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int read = channel.read(buffer);
if(read > 0) {
System.out.println("客服端传来消息: " + new String(buffer.array(),
0, read, StandardCharsets.UTF_8));
}
//12.给客户端回写数据
channel.write(ByteBuffer.wrap("我已经接收到了...".getBytes(StandardCharsets.UTF_8))) ;
channel.close();
}
//13.从集合中删除对应的事件, 因为防止二次处理. iterator.remove();
iterator.remove();
}
}
}