二、socket编程

178 阅读23分钟

socket编程

客户端与服务端创建tcp连接后,相互发送数据,如果数据量很大,会被分为多个报文段进行发送,报文段的大小可以通过ifconfig命令查看

image.png

图中的MTU的值表示1500个字节,说明这个网卡的一个报文段大小是1500个字节。

但是一个报文一个报文的发送速度太慢了,所以tcp有一个流量控制机制,可以一次性发送多个报文,只要服务端有能力接收。

linux内核在创建tcp连接后,会为客户端和服务端申请资源,其中包括存放数据的缓存,这个可以被称为窗口。客户端和服务端刚建立tcp连接时,会在报文中发送各自的空闲窗口大小。服务端发送空闲窗口大小是为了告诉客户端,此时可以接收多少数据,客户端会据此发送不会超过这个大小的数据。这个数据往往比一个报文段要大。这就是流量控制机制。

如果服务端的缓存已经满了,会向客户端发送空闲窗口大小为0,此时客户端会堵塞不发数据。直到服务端的进程处理了数据,缓存中有了多余的空间,服务端会向客户端发送一条报文,告诉客户端此时的空闲窗口大小,客户端再发送数据。

image.png

图中的win字段值就是空闲窗口大小。

socket编程的常见参数

ReceiveBufferSize:

服务端的接收缓存大小

SendBufferSize:

客户端的发送缓存大小

SoTimeout:

接收数据的超时时间

TcpNoDelay:

参数设置为true,表示不进行延迟发送,内核拿到数据就发送;设置为false,表示延迟发送,内核积攒数据到一定量后才会发送给服务端

KeepAlive:

参数设置为true,表示长连接,客户端和服务端长时间不传输数据,为了确保双方还是正常的,会定时发送保活报文;设置为false,表示短连接,超过一定时间不发送数据,连接就会断开

socket的系统调用

BIO的系统调用

public static void main(String[] args) throws Exception {
	// 这一步会调用内核的三个方法,socket、bind、listen,并且生成一个文件描述符fd1
	// 此时服务端已经绑定了IP和端口号,等待客户端来建立连接
	// 如果客户端来建立了连接,那么会另外生成一个文件描述符fd2。fd2表示已经建立好的连接
	ServerSocket server = new ServerSocket(9090,20);
	System.out.println("step1: new ServerSocket(9090) ");
	while (true) {
		// 这一步会调用内核的accept方法,拿到已经建立好的连接的文件描述符fd2
		// 如果调用accept方法时,还未建立好连接,就会在此处堵塞
		Socket client = server.accept();
		System.out.println("step2:client\t" + client.getPort());
		new Thread(new Runnable(){
			public void run() {
				InputStream in = null;
				try {
					in = client.getInputStream();
					BufferedReader reader = new BufferedReader(new InputStreamReader(in));
					while(true){
						// 这一步会调用内核的recv方法,读取数据,如果没有数据就会堵塞
						String dataline = reader.readLine();
						if(null != dataline){
							System.out.println(dataline);
						}else{
							client.close();
							break;
						}
					}
					System.out.println("客户端断开");
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}).start();
	}
}

socket在建立连接前后会生成两个文件描述符。第一个文件描述符是等待客户端的连接过来;第二个文件描述符是建立连接后生成的,通过这个文件描述符可以读写数据。

BIO效率低的原因是accept方法和read方法在调用内核提供的方法都是堵塞的,所以每一个连接过来都需要启动一个线程去处理,线程的启动是需要花费时间的,所以效率低。

linux系统内核后来也提供了不会堵塞的方法,java中的nio包中提供的网络编程语法就调用了这些方法,比BIO的效率要高

NIO的系统调用

public static void main(String[] args) throws Exception {
	LinkedList<SocketChannel> clients = new LinkedList<>();
	//创建服务端的socket
	ServerSocketChannel ss = ServerSocketChannel.open();
	ss.bind(new InetSocketAddress(9090));
	//将ServerSocketChannel设置调用内核中非堵塞的方法
	ss.configureBlocking(false);
	while (true) {
		//系统调用的还是accept方法,但是不会堵塞
		SocketChannel client = ss.accept();
		if (client == null) {
			//当还没有建立连接,内核的accept方法返回-1,java中的accept方法返回null
		    System.out.println("null.....");
		} else {
			//将SocketChannel设置调用内核中非堵塞的方法
			//read方法就不会堵塞了
			client.configureBlocking(false); 
			int port = client.socket().getPort();
			System.out.println("client..port: " + port);
			clients.add(client);
		}

		ByteBuffer buffer = ByteBuffer.allocateDirect(4096); 
		for (SocketChannel c : clients) {  
			int num = c.read(buffer);  
			if (num > 0) {
				buffer.flip();
				byte[] aaa = new byte[buffer.limit()];
				buffer.get(aaa);
				String b = new String(aaa);
				System.out.println(c.socket().getPort() + " : " + b);
				buffer.clear();
			}
		}
	}
}

由于nio调用的内核方法都是不会堵塞的,所以一个线程就可以处理多个io连接。相比bio,节省了创建线程的过程,效率更高。

多路复用器

nio解决了bio的堵塞问题,一个线程就可以处理多个io,节约资源。但是,nio依然存在问题,假如进程有1万个io,一个线程每次都需要遍历这1万个io,而且,实际情况是,每次遍历只有3~4个io是有数据的,其他都是无效遍历。每次遍历都会进行一次用户态和内核态的切换,很浪费资源。这就是nio的缺点。

多路复用器模型提升了nio的效率。用户程序只需要调用一次内核函数,将需要遍历的文件描述符给内核,内核中的多路复用器会遍历这些文件描述符,将准备就绪的io返回,如此一来,应用程序再遍历这些有效的io,提高效率。

image.png

在linux系统中,SELECT、POLL、EPOLL都是多路复用器模型

SELECT和POLL

SELECT和POLL非常的相似,它们都是让应用程序先进行一次系统调用,将所有的文件描述符发送给内核,由内核遍历得到准备好的文件描述符返回给应用程序,应用程序再遍历这些有效的文件描述符进行读写。

SELECT和POLL的区别是,SELECT对一次遍历的文件描述符个数是有限制的(好像是1024个),而POLL是没有限制的。

image.png

SELECT和POLL依然存在问题,因为每次都需要应用程序将所有的文件描述符发送给内核,内核自己不会保存,而且每次应用程序进行一次系统调用,内核都需要遍历所有的文件描述符,这就比较浪费效率和空间。

EPOLL

EPOLL是在网卡进行中断时,cpu执行中断函数执行一些操作,解决了SELECT和POLL存在的问题。

我们知道当网卡接收到数据后,会给cpu发送中断,cpu会响应中断,执行中断函数,将网卡缓存中的数据迁移到对应的文件描述符的缓存区中,方便应用程序来读取。

EPOLL是内核首先开辟了一块空间,这个空间中有红黑树和链表。红黑树存储了应用程序的所有文件描述符。当网卡接收到数据,向cpu发送中断,cpu响应中断开始迁移网卡缓存中的数据时,内核会将对应的文件描述符从红黑树中复制一份,放到空间的链表中,所以链表里存的是准备就绪的文件描述符。如此一来,应用程序进行一次系统调用,将文件描述符发送给内核后就不用重复发送了,而且可以得到准备就绪的文件描述符进行读写操作了,效率比SELECT和POLL有所提升。

image.png

epoll模型中有三个系统调用,分别是epoll_create,epoll_ctl,epoll_wait。

首先应用程序调用epoll_create,在内核中创建一块空间以及红黑树和链表。然后调用epoll_ctl,将文件描述符传递给内核,由内核存入红黑树中。如果红黑树中的文件描述符的状态发生改变了,内核会复制一份放到链表中。如此一来,应用程序调用epoll_wait,就可以拿到链表中准备就绪的文件描述符进行操作了。

内核创建了一块空间用于存储应用程序的文件描述符,以至于应用程序不用每次传递所有的文件描述符了。还有,当网卡接收到数据后,内核会将对应的文件描述符复制一份到链表中,如此一来,应用程序调用epoll_wait,内核不用遍历红黑树中的文件描述符,判断哪些是准备就绪的,直接将链表中的返回即可。epoll的这种机制解决了select和poll存在的问题。

java编程实战

使用多路复用器模型的代码如下:

public class SocketMultiplexingSingleThreadv1 {

    private ServerSocketChannel server = null;
    //多路复用器对象
    //linux内核提供了三种多路复用器,select、poll和epoll,默认情况下使用的是epoll
    //可以通过启动参数:-Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.EPollSelectorProvider进行修改
    private Selector selector = null;
    int port = 9090;

    public void initServer() {
        try {
            //生成监听的fd
            server = ServerSocketChannel.open();
            //设置成非阻塞
            server.configureBlocking(false);
            server.bind(new InetSocketAddress(port));
  
            //调用open方法
            //在select和poll模型中,相当于啥也没做
            //在epoll模型中,会调用epoll_create方法,在内核中开辟空间
            selector = Selector.open();
            //调用register方法
            //在select和poll模型中,相当于将监听fd放到JVM的一个集合中
            //在epoll模型中,会调用epoll_ctl方法,将监听fd放到内核空间的红黑树中
            server.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void start() {
        initServer();
        System.out.println("服务器启动了。。。。。");
        try {
            while (true) {  //死循环
                //可以得到多路复用器中有多少个fd
                Set<SelectionKey> keys = selector.keys();
                System.out.println(keys.size()+"   size");
                //调用select方法
                //在select和poll模型中,相当于将JVM中的fd集合传给内核,返回准备就绪的fd集合
                //在epoll模型中,会调用epoll_wait方法,返回准备就绪的fd集合
                //此方法返回了准备就绪fd集合的元素个数
                while (selector.select() > 0) {
                    //返回准备就绪的fd集合
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();  
                    Iterator<SelectionKey> iter = selectionKeys.iterator();
                    while (iter.hasNext()) {
                        SelectionKey key = iter.next();
                        //删除原集合中的元素
                        iter.remove();
                        if (key.isAcceptable()) {
                            //进入这里说明有客户端过来建立了连接
                            //acceptHandler方法会将建立了连接的fd放到select、poll的JVM集合中,或者是epoll的内核红黑树中
                            acceptHandler(key);
                        } else if (key.isReadable()) {
                            //进入这里说明有客户端发送数据过来
                            readHandler(key);
                        }
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void acceptHandler(SelectionKey key) {
        try {
            ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
            //进入这个方法,说明accept方法一定有返回值
            SocketChannel client = ssc.accept();
            client.configureBlocking(false);

            ByteBuffer buffer = ByteBuffer.allocate(8192);
            //将建立了连接的fd放到select、poll的JVM集合中,或者是epoll的内核红黑树中
            //而且这个fd规定了是读事件
            client.register(selector, SelectionKey.OP_READ, buffer);
            System.out.println("-------------------------------------------");
            System.out.println("新客户端:" + client.getRemoteAddress());
            System.out.println("-------------------------------------------");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void readHandler(SelectionKey key) {
        SocketChannel client = (SocketChannel) key.channel();
        ByteBuffer buffer = (ByteBuffer) key.attachment();
        buffer.clear();
        int read = 0;
        try {
            while (true) {
                read = client.read(buffer);
                if (read > 0) {
                    buffer.flip();
                    while (buffer.hasRemaining()) {
                        client.write(buffer);
                    }
                    buffer.clear();
                } else if (read == 0) {
                    break;
                } else {
                    client.close();
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

tcp四次挥手状态分析

tcp建立连接需要三次握手,断开连接需要四次挥手,握手和挥手的过程都是内核之间的交互。在四次挥手过程中,客户端与服务端有状态的变化,例如:CLOSE_WAIT,FIN_WAIT1,FIN_WAIT2,TIME_WAIT,LAST_ACK,CLOST等

image.png

客户端与服务端刚建立连接时,双方都处于ESTABLISHED状态,此时可以发送报文通讯。

当客户端想断开连接,会发送FIN报文给服务端,此时客户端会处于FIN_WAIT1状态,服务端接收到FIN报文后,会响应一个FIN+ACK报文,服务端会变成CLOST_WAIT状态。客户端接收到报文后会变成FIN_WAIT2状态。

此时,服务端的内核不会立即向客户端发送FIN报文,表示我也要断开连接,因为内核需要等应用程序将客户端发送过来的数据处理完成,等待应用程序调用close方法。如果程序员在代码中忘记写调用close方法关闭连接,那么客户端和服务端就一直处于这种状态,内核中的socket四元组就一直被占用,浪费资源。

所以等应用程序调用了close方法,告诉内核可以断开连接了,服务端内核就会向客户端发送FIN报文,并且自身状态变成LAST_ACK。客户端接收到报文后,会向服务端发送ACK报文,并且自身变成TIME_WAIT状态。

客户端的TIME_WAIT状态会持续报文最大存活时间的2倍的时间,因为客户端发送了ACK报文,无法确保服务端一定可以接收到。如果服务端没有接收到,服务端会重新发送FIN报文,此时客户端的资源没有被清除,就可以正常处理这个报文,给服务端再发送ACK,以确保四次挥手过程的完整性。

最后,客户端与服务端都完成四次挥手的过程,双方都会经历非常短暂的CLOSE状态。

使用多路复用器完成监听连接、读数据和写数据

public class SocketMultiplexingSingleThreadv1_1 {

    private ServerSocketChannel server = null;
    private Selector selector = null; 
    int port = 9090;

    public void initServer() {
        try {
			// 完成socket四元组的一半,等待客户端来连接,构建出完整的四元组
            server = ServerSocketChannel.open();
            server.configureBlocking(false);
            server.bind(new InetSocketAddress(port));
			// 内核中开辟空间
            selector = Selector.open();
			// 监听连接事件发送给内核
            server.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void start() {
        initServer();
        System.out.println("服务器启动了。。。。。");
        try {
            while (true) {
				// 获得准备就绪的连接,如果没有准备就绪的,线程会堵塞在这里
				// select方法还可以传入时间参数,表示只会堵塞一段时间,时间过后就会放行
                while (selector.select() > 0) {
					// 获取准备就绪的连接集合
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();
                    Iterator<SelectionKey> iter = selectionKeys.iterator();
                    while (iter.hasNext()) {
                        SelectionKey key = iter.next();
                        iter.remove();
                        if (key.isAcceptable()) {
							// 有客户端连接
                            acceptHandler(key);
                        } else if (key.isReadable()) {
							// 有读事件
//                            key.cancel();  // 将此fd从内核的红黑树中删除
                            readHandler(key);  

                        } else if(key.isWritable()){  
							// 有写事件
							// 只要将写事件添加到内核空间中,就会一直触发
							// 除非内核中的send_queue没有剩余空间了,就不会触发写事件
//                            key.cancel();  // 将此fd从内核的红黑树中删除
                            writeHandler(key);
                        }
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void writeHandler(SelectionKey key) {

		System.out.println("write handler...");
		SocketChannel client = (SocketChannel) key.channel();
		ByteBuffer buffer = (ByteBuffer) key.attachment();
		buffer.flip();
		while (buffer.hasRemaining()) {
			try {

				client.write(buffer);
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		buffer.clear();
		key.cancel();
		try {
			client.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
    }

    public void acceptHandler(SelectionKey key) {
        try {
            ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
            SocketChannel client = ssc.accept();
            client.configureBlocking(false);
            ByteBuffer buffer = ByteBuffer.allocate(8192);
			// 添加一个读fd
            client.register(selector, SelectionKey.OP_READ, buffer);
            System.out.println("-------------------------------------------");
            System.out.println("新客户端:" + client.getRemoteAddress());
            System.out.println("-------------------------------------------");

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void readHandler(SelectionKey key) {
		System.out.println("read handler.....");
		SocketChannel client = (SocketChannel) key.channel();
		ByteBuffer buffer = (ByteBuffer) key.attachment();
		buffer.clear();
		int read = 0;
		try {
			while (true) {
				read = client.read(buffer);
				if (read > 0) {
					client.register(key.selector(),SelectionKey.OP_WRITE,buffer);
					//关心  OP_WRITE 其实就是关心send-queue是不是有空间
				} else if (read == 0) {
					break;
				} else {
					client.close();
					break;
				}
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
    }

    public static void main(String[] args) {
        SocketMultiplexingSingleThreadv1_1 service = new SocketMultiplexingSingleThreadv1_1();
        service.start();
    }
}

多线程的多路复用器

单线程的多路复用器需要遍历内核返回的fd集合,对每个fd进行读写操作。其实可以使用多线程,当进行读或者写操作时,可以另起一个线程去处理。如此一来,集合中的fd事件就是并行执行的,效率会高一些。

代码如下:

public class SocketMultiplexingSingleThreadv2 {

    private ServerSocketChannel server = null;
    private Selector selector = null;  
    int port = 9090;

    public void initServer() {
        try {
            server = ServerSocketChannel.open();
            server.configureBlocking(false);
            server.bind(new InetSocketAddress(port));
            selector = Selector.open();
            server.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void start() {
        initServer();
        System.out.println("服务器启动了。。。。。");
        try {
            while (true) {
                while (selector.select(50) > 0) {
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();
                    Iterator<SelectionKey> iter = selectionKeys.iterator();
                    while (iter.hasNext()) {
                        SelectionKey key = iter.next();
                        iter.remove();
                        if (key.isAcceptable()) {
                            acceptHandler(key);
                        } else if (key.isReadable()) {
                            //key.cancel(); 
                            System.out.println("in.....");
                            //key.interestOps(key.interestOps() | ~SelectionKey.OP_READ);
                            readHandler(key);
                        } else if(key.isWritable()){  
                            //key.cancel();
                            //key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE);
                            writeHandler(key);
                        }
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void writeHandler(SelectionKey key) {
        new Thread(()->{
            System.out.println("write handler...");
            SocketChannel client = (SocketChannel) key.channel();
            ByteBuffer buffer = (ByteBuffer) key.attachment();
            buffer.flip();
            while (buffer.hasRemaining()) {
                try {
                    client.write(buffer);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            buffer.clear();
//            key.cancel();
//            try {
////                client.shutdownOutput();
//                  client.close();
//            } catch (IOException e) {
//                e.printStackTrace();
//            }
        }).start();
    }

    public void acceptHandler(SelectionKey key) {
        try {
            ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
            SocketChannel client = ssc.accept();
            client.configureBlocking(false);
            ByteBuffer buffer = ByteBuffer.allocate(8192);
            client.register(selector, SelectionKey.OP_READ, buffer);
            System.out.println("-------------------------------------------");
            System.out.println("新客户端:" + client.getRemoteAddress());
            System.out.println("-------------------------------------------");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void readHandler(SelectionKey key) {
        new Thread(()->{
            System.out.println("read handler.....");
            SocketChannel client = (SocketChannel) key.channel();
            ByteBuffer buffer = (ByteBuffer) key.attachment();
            buffer.clear();
            int read = 0;
            try {
                while (true) {
                    read = client.read(buffer);
                    System.out.println(Thread.currentThread().getName()+ " " + read);
                    if (read > 0) {
                        key.interestOps(SelectionKey.OP_READ);
                        client.register(key.selector(),SelectionKey.OP_WRITE,buffer);
                    } else if (read == 0) {

                        break;
                    } else {
                        client.close();
                        break;
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }).start();
    }

    public static void main(String[] args) {
        SocketMultiplexingSingleThreadv2 service = new SocketMultiplexingSingleThreadv2();
        service.start();
    }
}

代码中,另起了一个线程去执行accept事件、read事件和write事件的处理逻辑,以提高io效率。但是,代码执行后会发现一个问题,同一个read和write事件频繁被触发,导致会生成多个线程去执行同一个读写事件。

这个问题出现的原因是,另起一个线程去处理读写事件,而主线程依然循环遍历内核返回的fd集合,主线程发现同一读写事件的fd也在集合中,就会将其再次触发。

为什么已经在处理的读写事件的fd还在集合中呢?因为他们没有处理结束。对于读事件,只要内核的recv_queue中有数据,那么这个读事件的fd就还在内核空间的链表中,会被内核返回。对于写事件,只要内核的send_queue有剩余空闲,那么这个写事件的fd就还在内核空间的链表中,会被内核返回。

所以,为了使用多线程提高io效率,也要避免这种情况发生,需要对多线程的多路复用器代码进行优化。

多线程的多路复用器优化(register和select方法相互阻塞)

为了避免同一个读写事件重复处理的情况,我们让每个线程都独立拥有自己的多路复用器。如此一来,每个多路复用器就只有一个线程在循环处理集合中的事件,不会出现问题了。

SelectorThreadv1:

这个类是一个线程,拥有自己的多路复用器,会遍历多路复用器中的fd进行操作

public class SelectorThreadv1 implements Runnable{

    Selector selector = null;
    SelectorThreadGroupv1 stg;

    SelectorThreadv1(SelectorThreadGroupv1 stg){
        try {
            this.stg = stg;
            selector = Selector.open();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        //Loop
        while (true){
            try {
                // 堵塞在这里,select方法会导致register方法也无法执行下去
                int nums = selector.select();
                if(nums>0){
                    Set<SelectionKey> keys = selector.selectedKeys();
                    Iterator<SelectionKey> iter = keys.iterator();
                    while(iter.hasNext()){
                        SelectionKey key = iter.next();
                        iter.remove();
                        if(key.isAcceptable()){
                        }else if(key.isReadable()){
                        }else if(key.isWritable()){
                        }
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

SelectorThreadv1Group:

这个类是一个存放SelectorThreadv1对象的集合类

public class SelectorThreadGroupv1 {

    // 存放SelectorThreadv1的数组
    SelectorThreadv1[] sts;
    ServerSocketChannel server=null;
    AtomicInteger xid = new AtomicInteger(0);

    // 构造函数,入参表示绑定多路复用器线程的个数
    SelectorThreadGroupv1(int num){
        sts = new SelectorThreadv1[num];
        for (int i = 0; i < num; i++) {
            // 生成绑定多路复用器线程
            sts[i] = new SelectorThreadv1(this);
            // 启动线程
            new Thread(sts[i]).start();
        }
    }

    public void bind(int port) {
        try {
            server = ServerSocketChannel.open();
            server.configureBlocking(false);
            server.bind(new InetSocketAddress(port));
            // 将监听连接fd放到某个多路复用器中
            nextSelector(server);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void nextSelector(Channel c) {
        // 随机选择一个多路复用器
        SelectorThreadv1 st = next();
        ServerSocketChannel s = (ServerSocketChannel) c;
        try {
            // 将accept放到选择的多路复用器中,这里存在一个问题
            // 由于在SelectorThreadGroupv1的构造函数中,已经启动了线程
            // 启动的线程必然会调用select方法,并且在那里堵塞
            // 此处的线程又调用了register方法
            // register方法是会和select方法相互阻塞的
            // 因为register方法要访问内核中的空间,select也需要访问,内核可能对多线程访问这个空间加了锁类似的限制
            s.register(st.selector, SelectionKey.OP_ACCEPT);
            st.selector.wakeup();
        } catch (ClosedChannelException e) {
            e.printStackTrace();
        }
    }

    private SelectorThreadv1 next() {
        int index = xid.incrementAndGet() % sts.length;
        return sts[index];
    }

    public static void main(String[] args) {
        // 声明一个SelectorThreadGroupv1对象
        // 声明一个SelectorThreadGroupv1中会存放SelectorThreadv1对象
        // SelectorThreadv1对象是一个线程,线程中包含了自己的Selector
        SelectorThreadGroupv1 stg = new SelectorThreadGroupv1(2);
        // 绑定9999端口号,创建一个监听连接fd
        stg.bind(9999);
    }
}

上面的代码想要多线程处理自己的多路复用器中的io事件,但是,有个坑就是register方法居然会与select方法相互阻塞,导致想要往多路复用器中放入io事件是不可能的了。所以,需要优化代码,解决这个问题。

多线程的多路复用器优化(解决register和select方法相互阻塞)

SelectorThreadv1中增加一个阻塞队列,专门存放将要放到多路复用器中的io事件

public class SelectorThreadv1 implements Runnable{

    Selector selector = null;
    SelectorThreadGroupv1 stg;
    LinkedBlockingQueue<Channel> lbq = new LinkedBlockingQueue<>();

    SelectorThreadv1(SelectorThreadGroupv1 stg){
        try {
            this.stg = stg;
            selector = Selector.open();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        //Loop
        while (true){
            try {
                // 堵塞在这里,select方法会导致register方法也无法执行下去
                int nums = selector.select();
                if(nums>0){
                    Set<SelectionKey> keys = selector.selectedKeys();
                    Iterator<SelectionKey> iter = keys.iterator();
                    while(iter.hasNext()){
                        SelectionKey key = iter.next();
                        iter.remove();
                        if(key.isAcceptable()){
                        }else if(key.isReadable()){
                        }else if(key.isWritable()){
                        }
                    }
                }

                if(!lbq.isEmpty()){
                    // 从队列中取出Channel,准备将io事件放到多路复用器中
                    Channel c = lbq.take();
                    if(c instanceof ServerSocketChannel){
                        ServerSocketChannel server = (ServerSocketChannel) c;
                        server.register(selector,SelectionKey.OP_ACCEPT);
                    }else if(c instanceof  SocketChannel){
                        SocketChannel client = (SocketChannel) c;
                        ByteBuffer buffer = ByteBuffer.allocateDirect(4096);
                        client.register(selector, SelectionKey.OP_READ, buffer);
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

SelectorThreadGroupv1不会直接在nextSelector方法中用当前线程将io事件放到多路复用器中,因为当前线程与多路复用器绑定的线程之间没有通信,所以当前线程不知道其是否正在执行select方法。所以,当前线程将这件事交给与多路复用器绑定的线程去做,就不会有问题了。

public class SelectorThreadGroupv1 { 

    // 存放SelectorThreadv1的数组
    SelectorThreadv1[] sts;
    ServerSocketChannel server=null;
    AtomicInteger xid = new AtomicInteger(0);

    // 构造函数,入参表示绑定多路复用器线程的个数
    SelectorThreadGroupv1(int num){
        sts = new SelectorThreadv1[num];
        for (int i = 0; i < num; i++) {
            // 生成绑定多路复用器线程
            sts[i] = new SelectorThreadv1(this);
            // 启动线程
            new Thread(sts[i]).start();
        }
    }

    public void bind(int port) {
        try {
            server = ServerSocketChannel.open();
            server.configureBlocking(false);
            server.bind(new InetSocketAddress(port));
            // 将监听连接fd放到某个多路复用器中
            nextSelector(server);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void nextSelector(Channel c) {
        // 随机选择一个多路复用器
        SelectorThreadv1 st = next();
        ServerSocketChannel s = (ServerSocketChannel) c;
        // 将Channel放入阻塞队列中,等待线程去将io事件放到多路复用器中
        st.lbq.add(c);
        // 立刻唤醒阻塞的线程,让其处理队列中的Channel
        st.selector.wakeup();
    }

    private SelectorThreadv1 next() {
        int index = xid.incrementAndGet() % sts.length;
        return sts[index];
    }

    public static void main(String[] args) {
        // 声明一个SelectorThreadGroupv1对象
        // 声明一个SelectorThreadGroupv1中会存放SelectorThreadv1对象
        // SelectorThreadv1对象是一个线程,线程中包含了自己的Selector
        SelectorThreadGroupv1 stg = new SelectorThreadGroupv1(2);
        // 绑定9999端口号,创建一个监听连接fd
        stg.bind(9999);
    }
}

多线程的多路复用器进一步优化

对于io事件,accept与read和write不同,只要监听端口,accept就一直存在内核中,而且可以被多个客户端建立连接。如果监听的端口多,accept也是不止一个。客户端建立连接后,accept也没有什么业务代码要执行,只需要将read事件注册到多路复用器就可以了。

所以,进一步的代码优化是将accept事件专门用一个多路复用器处理。由于一个进程可能开启多个端口监听,每个端口可能会有多个客户端来建立连接。我们就每个端口给一个多路复用器。这样就和read已经write事件解耦了。

代码:

MainThread:

public class MainThread {

    public static void main(String[] args) {
        // 专门存放accept的多路复用器组
        // 有多少个监听端口,组的容量就是多少
        SelectorThreadGroup boss = new SelectorThreadGroup(3);

        // 专门存放read和write的多路复用器组
        SelectorThreadGroup worker = new SelectorThreadGroup(3);

        // 因为boss组中多路复用器的accept触发后,需要将read事件注册到worker多路复用器组中
        // 所以boss组中需要有worker组
        boss.setWorker(worker);

        boss.bind(9999);
        boss.bind(8888);
        boss.bind(7777);
    }
}

SelectorThreadGroup:

/**
 * SelectorThreadGroup对象分boss和worker
 * boss专门负责处理accept
 * worker专门负责处理read和write
 */
public class SelectorThreadGroup {

    // 绑定多路复用器的线程集合
    SelectorThread[] sts;
    ServerSocketChannel server = null;
    AtomicInteger xid = new AtomicInteger(0);

    // 存放read和write事件的多路复用器组(worker)
    // boss组中需要有worker组,因为绑定端口时,需要将accept事件注册到worker组的某个多路复用器上
    SelectorThreadGroup stg = this;

    // 给worker多路复用器组赋值
    public void setWorker(SelectorThreadGroup stg) {
        this.stg = stg;
        // 给只处理accept事件的线程中的多路复用器组赋值为worker
        // 因为这些线程在处理完accept事件后,需要将read/write事件注册到多路复用器中
        // 而read/write事件是由worker多路复用器组处理的
        // 所以需要在每个boss线程中存一份worker组
        for (SelectorThread st : sts) {
            st.setWorker(stg);
        }
    }

    SelectorThreadGroup(int num) {
        sts = new SelectorThread[num];
        // 初始化组中的每个线程
        for (int i = 0; i < num; i++) {
            // 对于boss组中的线程,以下代码是将boss组赋值给了线程中的SelectorThreadGroup成员
            // 对于worker组中的线程,以下代码是将worker组赋值给了线程中的SelectorThreadGroup成员
            // 不过对于boss组中的线程,在boss组对象调用setWorker方法后,会将SelectorThreadGroup成员从boss组换成worker组
            sts[i] = new SelectorThread(this);
            new Thread(sts[i]).start();
        }
    }

    public void bind(int port) {
        try {
            server = ServerSocketChannel.open();
            server.configureBlocking(false);
            server.bind(new InetSocketAddress(port));
            nextSelector(server);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void nextSelector(Channel c) {
        try {
            if (c instanceof ServerSocketChannel) {
                // 进入这里说明是accept
                SelectorThread st = nextBossThread();
                st.lbq.put(c);
                st.selector.wakeup();
            } else {
                // 进入这里说明是read/write
                SelectorThread st = nextWorkerThread();
                st.lbq.add(c);
                st.selector.wakeup();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private SelectorThread nextBossThread() {
        // 从boss组中选择一个线程
        int index = xid.incrementAndGet() % sts.length;
        return sts[index];
    }

    private SelectorThread nextWorkerThread() {
        // 从worker组中选择一个线程
        int index = xid.incrementAndGet() % stg.sts.length;
        return stg.sts[index];
    }
}

boss和worker的SelectorThreadGroup模型:

image.png

SelectorThread:

/**
 * SelectorThread对象分boss和worker
 * boss的SelectorThread对象只处理accept
 * worker的SelectorThread对象只处理read和write
 */
public class SelectorThread extends ThreadLocal<LinkedBlockingQueue<Channel>> implements Runnable {
  
    Selector selector = null;
    // 从线程自己的ThreadLocalMap中获取到LinkedBlockingQueue<Channel>对象
    LinkedBlockingQueue<Channel> lbq = get();

    // 无论是boss组还是worker组的线程,这里都是worker组对象
    SelectorThreadGroup stg;

    @Override
    protected LinkedBlockingQueue<Channel> initialValue() {
        // 继承ThreadLocal类
        // 将LinkedBlockingQueue<Channel>对象存到线程自己的ThreadLocalMap中,避免并发的情况
        return new LinkedBlockingQueue<>();
    }

    SelectorThread(SelectorThreadGroup stg) {
        try {
            // 线程在初始化的时候,对SelectorThreadGroup对象赋值
            // boss组线程赋boss组,worker组线程赋worker组
            // 不过boss组会在调用setWorker方法后,将boss组换成worker组
            this.stg = stg;
            // 开启多路复用器
            selector = Selector.open();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 设置线程的SelectorThreadGroup对象
    public void setWorker(SelectorThreadGroup stgWorker) {
        this.stg = stgWorker;
    }
  
    @Override
    public void run() {
        //死循环
        while (true) {
            try {
                // 没有io事件发生时,这里会堵塞
                int nums = selector.select();
                // 到这里有两种情况
                // 第一种:有io事件发生
                // 第二种:有线程调用了wakeup方法
                if (nums > 0) {
                    // 进入这里说明有io事件发生
                    // 获得io事件集合
                    Set<SelectionKey> keys = selector.selectedKeys();
                    Iterator<SelectionKey> iter = keys.iterator();
                    // 遍历集合
                    while (iter.hasNext()) {
                        SelectionKey key = iter.next();
                        iter.remove();
                        if (key.isAcceptable()) {
                            // accept连接事件发生
                            acceptHandler(key);
                        } else if (key.isReadable()) {
                            // read事件发生
                            readHander(key);
                        } else if (key.isWritable()) {
                            // write事件发生
                        }
                    }
                }
                // 如果nums<=0,那说明是有线程调用了wakeup方法
                if (!lbq.isEmpty()) {
                    // 进入这里说明有新的io事件要注册到多路复用器了
                    Channel c = lbq.take();
                    if (c instanceof ServerSocketChannel) {
                        // 进入这里说明是accept事件要注册
                        ServerSocketChannel server = (ServerSocketChannel) c;
                        server.register(selector, SelectionKey.OP_ACCEPT);
                        System.out.println(Thread.currentThread().getName() + " register listen");
                    } else if (c instanceof SocketChannel) {
                        // 进入这里说明是read/write事件要注册
                        SocketChannel client = (SocketChannel) c;
                        ByteBuffer buffer = ByteBuffer.allocateDirect(4096);
                        client.register(selector, SelectionKey.OP_READ, buffer);
                        System.out.println(Thread.currentThread().getName() + " register client: " + client.getRemoteAddress());
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    // 处理read事件
    private void readHander(SelectionKey key) {
        System.out.println(Thread.currentThread().getName() + " read......");
        ByteBuffer buffer = (ByteBuffer) key.attachment();
        SocketChannel client = (SocketChannel) key.channel();
        buffer.clear();
        while (true) {
            try {
                int num = client.read(buffer);
                if (num > 0) {
                    buffer.flip();  //将读到的内容翻转,然后直接写出
                    while (buffer.hasRemaining()) {
                        client.write(buffer);
                    }
                    buffer.clear();
                } else if (num == 0) {
                    break;
                } else if (num < 0) {
                    //客户端断开了
                    System.out.println("client: " + client.getRemoteAddress() + "closed......");
                    key.cancel();
                    break;
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    // 处理write事件
    private void acceptHandler(SelectionKey key) {
        System.out.println(Thread.currentThread().getName() + "   acceptHandler......");
        ServerSocketChannel server = (ServerSocketChannel) key.channel();
        try {
            SocketChannel client = server.accept();
            client.configureBlocking(false);
            stg.nextSelector(client);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

boss和worker的SelectoreThread模型:

image.png