-
socket是什么?
四元组,cip:cport + sip:sport
唯一确定一组连接,服务端不需要为每个客户端连接都分配一个端口
在linux上就是一个fd,可用netstat查看连接状态
-
建立连接的过程?
三次握手建立连接 - 确保双方确认连接
四次挥手断开连接 - 确保双方断开连接
-
TCP的拥塞控制?
通过滑动窗口控制,每次ack会返回一个可用窗口大小,别给我多发,多发我受不了。
-
BIO中,new serveSocket(9080,20),accept(),read(),内核调了什么方法?
socket = fd3,
bind(fd3,8090),
listen(fd3),
accept(fd3, -> fd5,//blocking
recv(fd5, //blocking 可通过多线程方式异步执行
BIO是同步阻塞模型
accept阻塞
recv阻塞
-
NIO
同步非阻塞模型
不阻塞,accept,recv
jdk:new io
os:noblocking io
操作系统提供了不阻塞的accept,recv
NIO代码:
public class SocketNIO { // what why how public static void main(String[] args) throws Exception { LinkedList<SocketChannel> clients = new LinkedList<>(); ServerSocketChannel ss = ServerSocketChannel.open(); //服务端开启监听:接受客户端 ss.bind(new InetSocketAddress(9090)); ss.configureBlocking(false); //重点 OS NONBLOCKING!!! //只让接受客户端 不阻塞 while (true) { //接受客户端的连接 Thread.sleep(1000); SocketChannel client = ss.accept(); //不会阻塞,没有客户端连接,OS层面返回-1,java层面返回NULL, //accept 调用内核了:1,没有客户端连接进来,返回值?在BIO 的时候一直卡着,但是在NIO ,不卡着,返回-1,NULL //如果来客户端的连接,accept 返回的是这个客户端的fd 5,client object //NONBLOCKING 就是代码能往下走了,只不过有不同的情况 if (client == null) { // System.out.println("null....."); } else { client.configureBlocking(false); //重点 socket(服务端的listen socket<连接请求三次握手后,往我这里扔,我去通过accept 得到 连接的socket>,连接socket<连接后的数据读写使用的> ) 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); // >0 -1 0 //不会阻塞 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(); } } } } }
挨个遍历accept的client,去R/W,每次遍历涉及到系统调用
问题:遍历成本在用户态和内核态的切换
-
c10k问题
每循环一次,O(n)的复杂度 -> recv调用,实际并不是每一个连接都有读事件发送,很多系统调用是无意义的,浪费系统资源
-
阻塞,非阻塞,同步,异步
阻塞:BLOCKING
非阻塞:NOBLOCKING
同步:应用程序自己完成:R/W
异步:kernel帮你完成:R/W,程序无感知,传入一个buffer,然后读buffer,不需要调用read
-
IO多路复用实现 (同步非阻塞)
select(1024 fds)
poll(没有1024 fds限制)
触发了一次系统调用,select方法把所有IO连接遍历一边,返回一个int值,表示有多少个IO事件发生。
问题:每次调用select函数,需要重传fds;都需要遍历全量的fds的一个复杂度
如何解决?在内核中开辟一块控件存fds,就不需要每次都传递了,直接将有IO事件发生的fds返回,就不需要遍历了。epoll来了
epoll
epoll有3个核心函数(epoll_create -> fd3, epoll_ctl(fd3,ADD,fd5,EPOLLIN), epoll_wait -> 读取有事件的io)
epoll_create:返回值是一个fd, 在内核开辟一个空间 epfd -> 红黑树,保存需要监听的fds,还会开辟一个链表空间,用于保存有事件发生的fds(如:accept,read)
epoll_ctl 维护红黑树上监听的fd集合
epoll_wait 获取所有事件的fds链表
在epoll之前IO中断的callback,kernel只是完成了将网卡发来的数据,走内核网络协议栈(2-数据链路层,3-网络层,4-传输层)最终关联到fd的buffer,
所以某一时间app询问内核是否有fd可R/W,会有状态返回。
epoll出现之后,如果kernel支持epoll,则kernel会在将网卡发来的数据最终关联到fd的buffer后,延伸一个操作,在epoll_create创建的红黑树上找到对应的fd,然后copy这个fd到内核空间的一个链表中,epoll_wait直接取这个链表的fds,就可以获取到有IO事件发生的fds。这个操作都是由内核完成的。
相比于select/poll,省去了遍历fds的过程,直接能返回有事件发生的fds。
public class SocketMultiplexingSingleThreadv1 { //马老师的坦克 一 二期 private ServerSocketChannel server = null; private Selector selector = null; //linux 多路复用器(select poll epoll kqueue) nginx event{} int port = 9090; public void initServer() { try { server = ServerSocketChannel.open(); server.configureBlocking(false); server.bind(new InetSocketAddress(port)); //如果在epoll模型下,open--》 epoll_create -> fd3 selector = Selector.open(); // select poll *epoll 优先选择:epoll 但是可以 -D修正 //server 约等于 listen状态的 fd4 /* register 如果: select,poll:jvm里开辟一个数组 fd4 放进去 epoll: epoll_ctl(fd3,ADD,fd4,EPOLLIN */ server.register(selector, SelectionKey.OP_ACCEPT); } catch (IOException e) { e.printStackTrace(); } } public void start() { initServer(); System.out.println("服务器启动了。。。。。"); try { while (true) { //死循环 Set<SelectionKey> keys = selector.keys(); System.out.println(keys.size()+" size"); //1,调用多路复用器(select,poll or epoll (epoll_wait)) /* select()是啥意思: 1,select,poll 其实 内核的select(fd4) poll(fd4) 2,epoll: 其实 内核的 epoll_wait() *, 参数可以带时间:没有时间,0 : 阻塞,有时间设置一个超时 selector.wakeup() 结果返回0 懒加载: 其实再触碰到selector.select()调用的时候触发了epoll_ctl的调用 */ while (selector.select() > 0) { Set<SelectionKey> selectionKeys = selector.selectedKeys(); //返回的有状态的fd集合 Iterator<SelectionKey> iter = selectionKeys.iterator(); //so,管你啥多路复用器,你呀只能给我状态,我还得一个一个的去处理他们的R/W。同步好辛苦!!!!!!!! // NIO 自己对着每一个fd调用系统调用,浪费资源,那么你看,这里是不是调用了一次select方法,知道具体的那些可以R/W了? //幕兰,是不是很省力? //我前边可以强调过,socket: listen 通信 R/W while (iter.hasNext()) { SelectionKey key = iter.next(); iter.remove(); //set 不移除会重复循环处理 if (key.isAcceptable()) { //看代码的时候,这里是重点,如果要去接受一个新的连接 //语义上,accept接受连接且返回新连接的FD对吧? //那新的FD怎么办? //select,poll,因为他们内核没有空间,那么在jvm中保存和前边的fd4那个listen的一起 //epoll: 我们希望通过epoll_ctl把新的客户端fd注册到内核空间 acceptHandler(key); } else if (key.isReadable()) { readHandler(key); //连read 还有 write都处理了 //在当前线程,这个方法可能会阻塞 ,如果阻塞了十年,其他的IO早就没电了。。。 //所以,为什么提出了 IO THREADS //redis 是不是用了epoll,redis是不是有个io threads的概念 ,redis是不是单线程的 //tomcat 8,9 异步的处理方式 IO 和 处理上 解耦 } } } } } catch (IOException e) { e.printStackTrace(); } } public void acceptHandler(SelectionKey key) { try { ServerSocketChannel ssc = (ServerSocketChannel) key.channel(); SocketChannel client = ssc.accept(); //来啦,目的是调用accept接受客户端 fd7 client.configureBlocking(false); ByteBuffer buffer = ByteBuffer.allocate(8192); //前边讲过了 // 0.0 我类个去 //你看,调用了register /* select,poll:jvm里开辟一个数组 fd7 放进去 epoll: epoll_ctl(fd3,ADD,fd7,EPOLLIN */ 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(); } } public static void main(String[] args) { SocketMultiplexingSingleThreadv1 service = new SocketMultiplexingSingleThreadv1(); service.start(); } }