动态每日更新算法题,想要学习的可以一起学习
还是先来看一下百度百科对于Socket的介绍:套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是IP地址与端口的组合。
我们将一个小区比作一台计算机,一台计算机里面跑了很多程序,怎么区分程序呢,用的是端口,就好像小区用门牌号区分每一户人家一样。手机送到小明家了,怎么进去呢?从大门进啊,怎么找到大门呢?门牌号呀。不就相当于从互联网来的数据找到接收端计算机后再根据端口判断应该给哪一个程序一样吗。小明家的入口就可以用小区地址+门牌号进行唯一表示,那么同样的道理,程序也可以用IP+端口号进行唯一标识。那么这个程序的入口就被称作Socket。
其实换句话来说,socket除了是入口还是通道。所以对于socket来说,我们并不难理解成单一的某一种功能,或者说把他看成是一个实例,建立连接的实例。
其实无非就是一个线程对应一个连接,当客户端向服务端发送消息后,就会进入阻塞状态,直到客户端收到服务端的消息之后才能进行下一步操作,这就是同步操作,也是一直被人诟病时间慢的原因。
尽管我们可以如上图一样,用多线程去连接多个通道,仍然还会存在资源浪费、速度慢等特点。
{
ExecutorService executor = Excutors.newFixedThreadPollExecutor(100);//线程池
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(8088);
while(!Thread.currentThread.isInturrupted()){//主线程死循环等待新连接到来
Socket socket = serverSocket.accept();//阻塞线程
executor.submit(new ConnectIOnHandler(socket));//为新的连接创建新的线程
}
class ConnectIOnHandler extends Thread{
private Socket socket;
public ConnectIOnHandler(Socket socket){
this.socket = socket;
}
public void run(){
while(!Thread.currentThread.isInturrupted()&&!socket.isClosed()){死循环处理读写事件
String someThing = socket.read()....//阻塞线程,当没有读到数据时会在这阻塞,读取数据
if(someThing!=null){
......//处理数据
socket.write()....//写数据
}
}
}
}
其实很好理解,BIO就是模拟客户端和服务端,代码中只是模拟了服务端,本机作为客户端访问,而连接就由Sokcet(套接字)来建立。
在代码片段的注释中,我们可以看到accept()方法其实就是一个阻塞方法,当没有新的连接建立时,程序会始终停止在这一行,且while循环也不会进行下去。只有当建立连接后,程序才会往下执行,这也是传统BIO速度慢且一直被人诟病的特点。
其实上述代码并不难理解,简单的IO流其实没有什么疑问,唯一想问的可能就是什么事Socket??
NIO全称java non-blocking IO(实际上是 new io),是指JDK 1.4 及以上版本里提供的新api(New IO) ,为所有的原始类型(boolean类型除外)提供缓存支持的数据容器,使用它可以提供非阻塞式的高伸缩性网络。
HTTP2.0使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比HTTP1.1大了好几个数量级。
NIO主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(选择器)。传统IO是基于字节流和字符流进行操作(基于流),而NIO基于Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道。
其实不管是channel还是Buffer或者是Selector只是量级的加快速度,真正使其升华的是非阻塞模式。举个例子,当客户端向服务端发送完消息后,客户端就不必再等待服务端的返回消息,取而代之的是有一个回调函数取处理这个返回请求,所以不必继续等待。
缓冲区Buffer
Buffer(缓冲区)是一个读取数据的内存块,可以理解成一个容器对象(含数组),该对象提供了一组方法,可以轻松的使用内存块。缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。channel提供从文件、网络读取数据的渠道,但是读取和写入数据必须都经由buffer。
通道Channel
1、NIO的通道类似于流,但有些区别:
区别1:通道可以同时进行读写,但是流只能读或者写
区别2:通道可以实现异步读写数据
区别3:通道可以从缓冲区读数据,也可以写数据到缓冲区
2、BIO中的stream是单向的,例如FileStream对象只能进行读取数据的操作,而NIO通道(channel)是双向的,可以读操作,也可以写操作。
3、channel是NIO的一个接口,public interface Channle extends Closeable{}
4、常用的channel类有:FileChannel、DatagramChannel、ServerSocketChannel和SocketChannel。
5、FileChannel用于数据读写,DatagramChannel用于UDP的数据读写,ServerSocketChannel和SocketChannel用于TCP的数据读写。
选择器Selector
1、Java的NIO是非阻塞的IO方式,可以用一个线程,处理多个客户端连接,就会使用到selector。
2、selector能够检测多个注册的通道上是否有事件发生(注意:多个channel以事件的方式可以注册到同一个selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理,这样就可以使用一个单线程去管理多个通道,也就是管理多个请求和连接。
3、只有在连接通道,真正有读写事件发生时,才会进行读写,就大大减少系统的开销,并且不必为每个连接都创建一个线程,不用去维护多个线程。
4、避免了多线程之间的上下文切换导致的开销。
服务器端
public void selector() throws IOException {
//先给缓冲区申请内存空间
ByteBuffer buffer = ByteBuffer.allocate(1024);
//打开Selector为了它可以轮询每个 Channel 的状态
Selector selector = Selector.open();
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);//设置为非阻塞方式
ssc.socket().bind(new InetSocketAddress(8080));
ssc.register(selector, SelectionKey.OP_ACCEPT);//注册监听的事件
while (true) {//内循环
Set selectedKeys = selector.selectedKeys();//取得所有key集合
Iterator it = selectedKeys.iterator();
while (it.hasNext()) {
SelectionKey key = (SelectionKey) it.next();
if ((key.readyOps() & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT) {
ServerSocketChannel ssChannel = (ServerSocketChannel) key.channel();
SocketChannel sc = ssChannel.accept();//接受到服务端的请求
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ);
it.remove();
} else if
((key.readyOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ) {
SocketChannel sc = (SocketChannel) key.channel();
while (true) {
buffer.clear();
int n = sc.read(buffer);//读取数据
if (n <= 0) {
break;
}
buffer.flip();
}
it.remove();
}
}
}
}
客户端
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.SocketChannel;
import java.util.Iterator;
/**
* NIO客户端
*/
public class NIOClient {
//通道管理器
private Selector selector;
/**
* 获得一个Socket通道,并对该通道做一些初始化的工作
* @param ip 连接的服务器的ip
* @param port 连接的服务器的端口号
* @throws IOException
*/
public void initClient(String ip,int port) throws IOException {
// 获得一个Socket通道
SocketChannel channel = SocketChannel.open();
// 设置通道为非阻塞
channel.configureBlocking(false);
// 获得一个通道管理器
this.selector = Selector.open();
// 客户端连接服务器,其实方法执行并没有实现连接,需要在listen()方法中调
//用channel.finishConnect();才能完成连接
channel.connect(new InetSocketAddress(ip,port));
//将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_CONNECT事件。
channel.register(selector, SelectionKey.OP_CONNECT);
}
/**
* 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
* @throws IOException
*/
public void connect() throws IOException {
// 轮询访问selector
while (true) {
// 选择一组可以进行I/O操作的事件,放在selector中,客户端的该方法不会阻塞,
//这里和服务端的方法不一样,查看api注释可以知道,当至少一个通道被选中时,
//selector的wakeup方法被调用,方法返回,而对于客户端来说,通道一直是被选中的
selector.select();
// 获得selector中选中的项的迭代器
Iterator ite = this.selector.selectedKeys().iterator();
while (ite.hasNext()) {
SelectionKey key = (SelectionKey) ite.next();
// 删除已选的key,以防重复处理
ite.remove();
// 连接事件发生
if (key.isConnectable()) {
SocketChannel channel = (SocketChannel) key.channel();
// 如果正在连接,则完成连接
if(channel.isConnectionPending()){
channel.finishConnect();
}
// 设置成非阻塞
channel.configureBlocking(false);
//在这里可以给服务端发送信息哦
channel.write(ByteBuffer.wrap(new String("向服务端发送了一条信息").getBytes("utf-8")));
//在和服务端连接成功之后,为了可以接收到服务端的信息,需要给通道设置读的权限。
channel.register(this.selector, SelectionKey.OP_READ); // 获得了可读的事件
} else if (key.isReadable()) {
read(key);
}
}
}
}
/**
* 处理读取服务端发来的信息 的事件
* @param key
* @throws IOException
*/
public void read(SelectionKey key) throws IOException{
//和服务端的read方法一样
// 服务器可读取消息:得到事件发生的Socket通道
SocketChannel channel = (SocketChannel) key.channel();
// 创建读取的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(512);
channel.read(buffer);
byte[] data = buffer.array();
String msg = new String(data).trim();
System.out.println("客户端收到信息:" + msg);
ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes("utf-8"));
channel.write(outBuffer);// 将消息回送给客户端
}
/**
* 启动客户端测试
* @throws IOException
*/
public static void main(String[] args) throws IOException {
NIOClient client = new NIOClient();
client.initClient("localhost",8000);
client.connect();
}
}
总结一下流程就是:
(1)在内核数据没有准备好的阶段,用户线程发起IO请求时,立即返回。用户线程需要不断地发起IO系统调用。
(2)内核数据到达后,用户线程发起系统调用,用户线程阻塞。内核开始复制数据。它就会将数据从kernel内核缓冲区,拷贝到用户缓冲区(用户内存),然后kernel返回结果。
(3)用户线程才解除block的状态,重新运行起来。经过多次的尝试,用户线程终于真正读取到数据,继续执行。