telnet是什么意思,telnet意思

  

  网络进程包括计算机组成、网络通信、操作系统、应用API等多个方面。   

  

  这次只讨论操作系统层面以上的网络IO基础。   

  

  从操作系统层面来说,网络IOsocketsocket主要包括五个要素:通信协议、客户端ip、客户端端口、服务器ip、服务器端口;可以理解为从应用层到传输层的一个抽象层,操作系统为应用提供很多socket相关的系统调用,方便网络通信。   

  

  您可以使用netstat命令检查网络连接。   

  

  检查网络连接。如果你之前进行过网络通信,比如调用curl www.baidu.com,然后检查网络连接,你会看到socket的五个元素,如下图所示:   

  

     

  

  文件描述符操作系统将socket连接映射成一个文件描述符(fd),socket的读写转换成fd的读写,以及进程的输入输出。抽象地说,在linux下创建一个socket连接,通过fd读写,可以更形象地理解:   

  

  #与百度建立socket连接,读写到文件描述符8 exec 8/dev/tcp/www . Baidu . com/80 # 8是一个文件描述符,代表iostream,内核建立socket连接#输出一段文字到上面的socket文件描述符,也就是发送TCP数据,从应用层发送数据到传输层。是socket应用层到传输层的抽象echo -e 'GET/HTTP/1.1\n' 1 8 #。从文件描述符8中输入它。第三条命令执行后,猫0 8会得到百度首页对应的内容。   

  

     

  

  如下图所示,C1、C2和C3是三个客户端,服务器是一个服务器。建立连接和读写数据的过程如下:   

  

  启动服务器,创建套接字,并绑定地址以获得S-fd服务器文件描述符。客户端通过服务器的套接字地址进行TCP三次握手连接。成功后,客户端生成表示socket连接的文件描述符(图中客户端的c1-fd、c2-fd、c3-fd),服务器S-fd读写进行三次握手。连接成功后,在服务器端生成一个表示客户端套接字连接的文件描述符(图中服务器的c1-fd、c2-fd、c3-fd)。客户端和服务器通过socket连接发送和接收数据(即读写c1-fd,c2-fd,c3-fd)。   

  

  IO阶段   

  

  从CPU工作的角度来看,网络IO的读取过程可以分为两个阶段。   

  

  从网卡读取数据到内核缓冲区;(需要发起IO请求,等待数据就绪)同样,从内核缓冲区复制数据到用户空间,写数据的过程可能也需要等到内核socket缓冲区满了,因为图中红色的缓冲内存。   

  

  综上所述,网络IO和本地文件IO的区别在于,读写过程可能需要一个等待过程,这有助于理解后面写网络IO程序的阻塞概念。   

  

  从Java网络IO程序到系统调用,操作系统为应用程序提供一系列系统调用,实现套接字连接、读写数据等。具体来说,它包括以下类别:   

  

  Socket # Create socketbind #服务器绑定地址listen # Monitor accept #服务器接收客户端连接recvfrom/read # Read数据。这里我们将从几个Java应用程序,结合它们运行时所做的系统调用,来了解网络IO的过程。   

  

  BIO,Blocking IO的简称,指阻塞木卫一。这里,我们通过一个java程序的运行,分析操作系统级是如何实现Bio服务器的。   

  

  program//bioserver . Java server socket server socket=new server socket(8081);While (true){ //接受连接,阻塞到连接的最终socket=server socket . Accept();//新线程(()-{ try { InputStream InputStream=socket . getinputstream();while (true){字节b   

ytes = new byte<1024>; // 从socket连接中读取数据,阻塞至有数据可读 if (inputStream.read(bytes) > 0){ System.out.println(String.format("got message: %s", new String(bytes))); } } } catch (IOException e) { e.printStackTrace(); } // }).start();}启动上面是一个简单的Java BIO程序,我们可以在执行它时同时使用strace命令查看程序运行时所进行的系统调用(Linux下)

  

strace -ff -o out java BIOServer # 查看进程的线程对内核进行了那些调用执行上面命令后,我们会得到几个out为前缀的文件,这些文件代表程序运行过程中不同的线程产生的系统调用,在主线程所打印的系统调用中可以找到我们上面提到过的几个关键的系统调用(其实会打印大量的系统调用,这里只列出关键的几个):

  

socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 7 # 创建一个socket,返回值7即代表这个server-socket在进程中的文件描述符bind(7, {sa_family=AF_INET, sin_port=htons(8081), sin_addr=inet_addr("0.0.0.0")}, 16) = 0 # 绑定地址,可以看到传入了socket系统调用返回的文件描述符,同样可以看到我们程序中的8081端口,地址0.0.0.0代表这是一个server,允许其它客户端进行连接listen(7, 50) = 0 # listen命令,同样使用到了文件描述符7,第二个参数50,实际代表了允许等待TCP三次握手的客户端队列长度poll(<{fd=7, events=POLLIN|POLLERR}>, 1, -1 # 对应我们程序中accept方法的调用,这里会阻塞,因为我们的程序刚刚启动,没有客户端进行连接以上几个关键的系统调用便是Linux操作系统为应用程序在系统层面上提供的,以方便应用程序创建一个Server。上面的注释中已经详细介绍了每个系统调用的含义,其实可以去 Linux man pages查看每个系统调用的详细文档,比如listen系统调用的第二个参数的含义在官方文档中的描述:

  

int listen(int sockfd, int backlog);The backlog argument defines the maximum length to which the queue ofpending connections for sockfd may grow. If a connection requestarrives when the queue is full, the client may receive an error withan indication of ECONNREFUSED or, if the underlying protocol supportsretransmission, the request may be ignored so that a later reattemptat connection succeeds.连接如上所述,当前程序阻塞在了accept方法处,我们使用telnet尝试连接:

  

telnet localhost 8081 然后去主线程的系统调用打印中查看,下面可以看到本来阻塞在poll系统调用的程序接着执行了,这里仍然列出几个关键的系统调用进行分析:

  

poll(<{fd=7, events=POLLIN|POLLERR}>, 1, -1) = 1 (<{fd=7, revents=POLLIN}>) # 阻塞调用返回 1accept(7, {sa_family=AF_INET, sin_port=htons(58972), sin_addr=inet_addr("127.0.0.1")}, <16>) = 8 # TCP三次握手成功,客户端连接创建成功,返回的8,是一个新的文件描述符,代表着服务端的客户端连接recvfrom(8, # 从文件描述符8(即客户端连接)中读取数据,阻塞因为我们只是连接成功,并没有发送数据,所以java程序主线程阻塞在了read方法处,对应系统调用recvfrom阻塞,系统调用打印停止。

  

读写接下来在客户端连接处发送数据aaaa,再查看系统调用:

  

recvfrom(8, "aaaa\r\n", 1024, 0, NULL, NULL) = 6可以看到recvfrom阻塞结束,成功读取到了我们在客户端发送的数据,返回值为读取到的字节数,这一步结束返回后,操作系统已经将读取到的数据拷贝到了java应用程序的内存区域,即数据到了java中的字节数组对象中。

  

问题BIO程序编写比较简单,简单的实现了服务端接受连接、读取数据等功能。但通过上面的分析,可以看到无论是应用程序层面,还是操作系统层面,程序存在线程阻塞的问题,且分别有接受连接、读取数据两处阻塞。如果我们的程序只有一个主线程,可以发现只能处理一个客户端连接,因为服务端不知道客户端何时发送数据,只能在没数据的时候也阻塞在read方法(系统层面的recvfrom系统调用)处。

  

为了可以使Server接受并处理多个客户端连接,一种解决方案是为每一个客户端连接创建一个线程,这样就解决了阻塞造成的问题。但是,现在的服务端应用程序一般对于客户端并发量要求较高,如果为每一个客户端连接创建一个线程,必然需要创建很多的线程,而线程是非常宝贵的资源,这样的程序设计浪费线程资源,且并发不能达到要求。

  

可以发现,问题出现在了阻塞上,如果解决了阻塞的问题,我们便可以有少量线程处理大量并发的解决方案。

  

无论是操作系统层面,还是java的api层面,其实都已经为我们开发者提供了非阻塞IO的支持。下面就对一个NIO程序进行分析。

  

NIONIO,在java中表示New IO,指新的IO操作方式(基于管道和缓冲区);在操作系统层面理解为Non-blocking IO,指的是非阻塞IO。java中的新IO同样提供了非阻塞IO的编程方式。下面的程序按照上面同样的流程分析一个NIO的Server程序是如何实现的:

  

程序这是一个简单的非阻塞的NIO Server程序,但编程难度相对BIO Server程序有所增加,可以将其与之前分析的BIO Server程序进行对比,必要的几步,比如开启服务端,绑定地址,接受连接,读取数据,其实仍然一一对应存在,变化不过是API层面的变化。

  

但该程序与BIO Server程序不同的地方在于,服务端开启连接和接受到客户端连接时,均调用了configureBlocking(false)方法设置为非阻塞,在接受连接的accept和read后,均判断了是否接受到连接或是否读取到数据,这也正是非阻塞与阻塞的最大区别。

  

// NIOSimpleServer.java// 保存客户端连接List<SocketChannel> socketChannelList = new ArrayList<>();// 开启服务端连接ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.bind(new InetSocketAddress("localhost", 8080));// 设置为非阻塞serverSocketChannel.configureBlocking(false);// 死循环,先尝试接受连接,再保存连接,并每次循环后对已有的连接进行处理while (true){ TimeUnit.MILLISECONDS.sleep(1000); // 因为非阻塞,无论有没有客户端进行连接,这一步将立即返回,所以下一步需要判断是否返回为null SocketChannel socketChannel = serverSocketChannel.accept(); if (socketChannel != null) { System.out.println("connect success"); // 设置为非阻塞 socketChannel.configureBlocking(false); socketChannelList.add(socketChannel); } for (SocketChannel socketChannel1 : socketChannelList) { ByteBuffer byteBuffer = ByteBuffer.allocate(1024); // 从客户端连接中读取数据,因为设置了非阻塞,无论有没有数据可读,这一步将立即返回,所以下一步需要判断是否读取到了数据 int read = socketChannel1.read(byteBuffer); if (read > 0) { byteBuffer.flip(); System.out.println(String.format("got message: %s", new String(byteBuffer.array()))); } }}启动同样,我们使用strace命令启动NIOSimpleServer程序在执行它时同时,查看程序运行时所进行的系统调用

  

strace -ff -o out java NIOSimpleServer同样,在主线程所打印的系统调用中可以找到我们上面提到过的几个关键的系统调用:

  

socket(AF_INET6, SOCK_STREAM, IPPROTO_IP) = 4 # 创建一个socket,返回值4即代表这个server-socket在进程中的文件描述符bind(4, {sa_family=AF_INET6, sin6_port=htons(8080), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "::ffff:127.0.0.1", &sin6_addr), sin6_scope_id=0}, 28) = 0 # 绑定地址,可以看到传入了socket系统调用返回的文件描述符,同样可以看到我们程序中的8080端口listen(4, 50) = 0 # listen命令,同样使用到了文件描述符7,第二个参数50,实际代表了允许等待TCP三次握手的客户端队列长度fcntl(4, F_SETFL, O_RDWR|O_NONBLOCK) = 0 # 设置socket连接非阻塞accept(4, 0x7f56140f5420, <28>) = -1 EAGAIN (资源暂时不可用) # 对应我们程序中accept方法的调用,这里不会阻塞,看到立马返回了-1,代表无连接,程序接着向下执行accept(4, 0x7f56140f5420, <28>) = -1 EAGAIN (资源暂时不可用)accept(4, 0x7f56140f5420, <28>) = -1 EAGAIN (资源暂时不可用)... # 无客户端连接时,会一致循环的accept下去,每次都返回-1,代表无连接可以看到其实非阻塞IO程序的启动过程涉及到的系统调用与阻塞IO程序基本相同,只是不再调用阻塞的poll,而是直接调用接受连接的accept,如果没有连接会返回-1,java程序中使用了死循环,所以没有客户端连接的情况下,一致循环进行accept系统调用。

  

连接同样,使用telnet尝试连接:

  

telnet localhost 8080然后去主线程的系统调用打印中查看,下面可以看到本来一直在进程循环accept并返回-1程序的一次accept返回类不一样的值,这里仍然列出几个关键的系统调用进行分析:

  

accept(4, {sa_family=AF_INET6, sin6_port=htons(40896), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "::ffff:127.0.0.1", &sin6_addr), sin6_scope_id=0}, <28>) = 5 # TCP三次握手成功,客户端连接创建成功,返回的8,是一个新的文件描述符,代表着服务端的客户端连接 fcntl(5, F_SETFL, O_RDWR|O_NONBLOCK) = 0 # 设置通道非阻塞accept(4, 0x7f56140df710, <28>) = -1 EAGAIN (资源暂时不可用)read(5, 0x7f56140f6440, 1024) = -1 EAGAIN (资源暂时不可用)accept(4, 0x7f56140df710, <28>) = -1 EAGAIN (资源暂时不可用)read(5, 0x7f56140f6440, 1024) = -1 EAGAIN (资源暂时不可用)...因为我们只是连接成功,并没有发送数据,所以程序每次read都会返回-1,没有数据可读,但是因为设置了非阻塞,所以没有数据可读并不会导致线程阻塞停下,而是不停的循环accept(接收新连接)和read(从已有连接中读取数据)。

  

读写接下来在客户端连接处发送数据aaa,再查看系统调用:

  

read(5, "aaa\r\n", 1024) = 5accept(4, 0x7f5614129d80, <28>) = -1 EAGAIN (资源暂时不可用)read(5, 0x7f56140f6440, 1024) = -1 EAGAIN (资源暂时不可用)accept(4, 0x7f5614129d80, <28>) = -1 EAGAIN (资源暂时不可用)read(5, 0x7f56140f6440, 1024) = -1 EAGAIN (资源暂时不可用)...可以看到其中一次read读取到了发送的数据aaa,返回值为读取到的字节数。同时,在读取数据完成后,按照程序的死循环写法,立马又开始循环的进行accept和read系统调用,尝试接受连接和读取从客户端连接中读取数据,也证明了这个程序是非阻塞的。

  

这里会发现,无论是阻塞IO还是非阻塞IO,操作系统底层为我们提供的支持,即系统调用函数,基本上是一样的。不同的点就在于,在创建socket连接,产生文件描述符那一步,是否通过fcntl系统调用设置了socket连接非阻塞。如果设置了非阻塞,操作系统在应用程序调用accept、read时,便不会阻塞,会直接返回,如果没数据返回的是-1,表示资源暂时不可用,如果有数据便会返回一个非负数,表示文件描述符(accept)或读取到的字节数(read)。向socket写入数据这里没有演示,但是可以类比,write系统调用向socket写入数据并返回已经写入的字节数。

  

问题通过这样一套操作,虽然略微增加了编码的复杂度,但是可以看到,我们已经解决了BIO Server程序的一些问题,我们现在仅仅使用一个线程,不仅可以不停的接收连接,还可以处理多个客户端连接的读写数据。

  

但是,这个程序也存在这一些问题。第一问题,我们使用了非阻塞,导致线程基本上需要不停的使用CPU,无论有没有新连接,有没有数据需要处理,每次循环都会把所有的socket连接轮询一遍。这就导致,我们虽然没有浪费更多的线程资源,但是会浪费许多的CPU资源。

  

第二个问题,我们将已经连接的客户端保存在了集合中,随着客户端连接的增加,这个集合会越来越大,每次循环我们都需要遍历这个结合,调用socketChannel的read尝试读取,而这个read操作涉及到系统调用read,一旦有系统调用,就涉及到了CPU在用户态和内核态之间的切换,这个切换是有一定性能代价的。

  

为了解决这两个问题,可以使用IO多路复用技术。

  

IO多路复用IO多路复用可以实现一次系统调用,由操作系统内核检查多个socket连接的状态,并且可以设置线程阻塞至关心的事件在socket连接上发生为止,也就解决了前面NIO的程序的两个问题。

  

操作系统提供了三个常用的系统调用select、poll、epoll来实现IO多路复用,下面分别说明。

  

  

select之前非阻塞IO的java程序将客户端连接保存在集合中,在应用程序层面进行遍历尝试读取数据,循环进行系统调用。select系统调用可以理解为将遍历查询是否有可读/可写等事件这一步放在操作系统内核完成,且可以阻塞timeout事件至有感兴趣的事件发生后返回,提高了性能。

  

// select函数的定义,nfds为fd最大 + 1,readfds、writefds、exceptfds分别为所关心的读事件、写事件、异常事件的文件描述符,timeout为阻塞时间;返回值为整数代表有事件的文件描述符个数;具体那个有事件在对应fd_set中获得int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); select使用IO多路复用的方式一定程度上解决了非阻塞IO程序的两个问题,但是其也存在者一些问题:

  

调用时直接传入fd_set监听描述符集合,受FD_SETSIZE限制,linux系统下默认1024;调用后阻塞,遍历描述符集合,找到就绪的,随着监听的文件描述符多时,效率会降低;pollpoll系统调用的作用与select基本一致,同样需要传入相关的文件描述符和事件,内核进行轮询,返回是否有事件发生并将事件设置在传入的参数中。

  

poll解决了select的监听文件描述符数量受限的问题,因为不再使用fd_set,而是使用结构体数组。

  

但像上面的图中描述,poll仍然需要每次调用传入所有关心的文件描述符,由内核进行遍历检查事件。

  

// poll函数的定义,fds为一个结构体数组int poll(struct pollfd *fds, nfds_t nfds, int timeout);struct pollfd { int fd; /* file descriptor 要监听的文件描述符*/ short events; /* requested events to watch ,感兴趣的事件,如果为负,将不检测*/ short revents; /* returned events witnessed,所发生的事件 */};epollepoll 同样是操作系统提供的IO多路复用的函数,解决与select、poll相同的问题,但相对后两者要更加强大。结合着上面的图和epoll的几个函数的定义介绍一下epoll使用的过程:

  

int epoll_create(int size); // 创建一个管理约size个需要监听的fd的fd;int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // 监听一个fd,放在红黑树中,一旦就绪,内核会采用类似callback的回调机制,激活这个描述符,再调用epoll_wait时便得到通知int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); // 查询一波事件epoll具体包含epoll_create、epoll_ctl、epoll_wait三个函数

  

首先调用epoll_create创建一个epoll实例,返回一个文件描述符epfd,这个文件描述符将用来管理需要监听事件的文件描述符使用epoll_ctl向epfd注册要监听的文件描述符(即socket连接),epoll_event中包含要监听的文件描述符、要监听的事件、一些配置项等;这一步之后,epoll将要监听的文件描述符加入到一个红黑树中,当事件就绪,将这个文件描述符放入到另一个就绪集合中应用程序调用epoll_wait,直接返回2中的就绪集合可以发现epoll有下面几个特点:

  

监听的fd数量基本不受限制,上限为打开文件的个数IO效率不会随监听的fd数量增长而下降,不需要遍历,采用每个fd回调通知方式epoll_wait查询是否有就绪事件时不需要复制fd,因为在之前的epoll_ctl中已经注册另外,epoll支持对于事件就绪的一些配置,比如支持边缘触发(Edge-triggered),关于事件就绪后的通知,epoll有两种处理方式

  

LT(Level-triggered)模式:就绪后如果不操作,会继续通知;(select、poll即java NIO均是这种模式)ET(Edge-triggered)模式:就绪后仅通知一次;这种模式仅epoll支持,这种模式下,比如有2M数据就绪,而应用程序只读取了1M,下次再调用epoll_wait,这个文件描述符不会出现在就绪集合中,剩下的数据只能应用程序自行循环读取(直到读到特殊的标记表示结束),nginx使用到多路复用程序Java NIO提供了多路复用的支持,主要使用了Selector类结合非阻塞IO程序中的几个类进行编码,下面是一个多路复用程序的主要代码:

  

// NIOServer.javaSelector selector = Selector.open();// 省略非阻塞IO程序中重复代码serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);// 查询事件selector.select();Set<SelectionKey> selectionKeys = selector.selectedKeys();// 处理客户端连接事件if (selectionKey.isAcceptable()) { SocketChannel channel = ((ServerSocketChannel) selectionKey.channel()).accept(); channel.configureBlocking(false); SelectionKey readKey = channel.register(selector, SelectionKey.OP_READ);} else if (selectionKey.isReadable()) { // 处理读取数据事件 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); SocketChannel channel = (SocketChannel) selectionKey.channel(); channel.read(byteBuffer); byteBuffer.flip(); System.out.println(String.format("got message: %s", new String(byteBuffer.array())));}类似前面的分析,我们使用strace启动这个程序,并连接,可以看到关键的系统调用如下:

  

epoll_create(256) = 6 # 创建socket(AF_INET6, SOCK_STREAM, IPPROTO_IP) = 7bind(7, {sa_family=AF_INET6, sin6_port=htons(8080), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "::ffff:127.0.0.1", &sin6_addr), sin6_scope_id=0}, 28) = 0listen(7, 50) = 0 # 监听server socketepoll_ctl(6, EPOLL_CTL_ADD, 7, {EPOLLIN, {u32=7, u64=362164703394267143}}) = 0epoll_wait(6, <{EPOLLIN, {u32=7, u64=362164703394267143}}>, 8192, -1) = 1 # 查询事件阻塞accept(7, {sa_family=AF_INET6, sin6_port=htons(37078), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "::ffff:127.0.0.1", &sin6_addr), sin6_scope_id=0}, <28>) = 8 # 接收客户端连接epoll_ctl(6, EPOLL_CTL_ADD, 8, {EPOLLIN, {u32=8, u64=352448473758433288}}) = 0 # 注册监听客户端连接上事件read(8, "aaaa\r\n", 1024) = 6 # 读到数据epoll_wait(6, # 下一次循环阻塞可以看到java中的Selector即使用了操作系统提供的epoll IO多路复用模式。

  

总结首先介绍了网络IO在操作系统层面上的表现,通过一个实例理解socket连接和文件描述符等关系;并根据网络IO的过程介绍了网络IO存在等待就绪的特点;然后根据Java的网络IO程序分别分析了阻塞IO(BIO)操作系统上的主要运行过程,根据BIO存在的问题然后同样分析了非阻塞IO(NIO)的运行过程;最后介绍了操作系统层面提供的IO多路复用技术,并分析java多路复用程序是如何使用epoll的

相关文章