「NIO源码」JavaNIO源码 & JNI分析一:linux API介绍

2,445 阅读5分钟

我正在参加「掘金·启航计划」

最近重温网络的时候,突然发现,底层就那么些接口,java肯定也是封装了底层接口,看过我前面Nio相关的小伙伴肯定知道对这些类有点影响(Buffer,Channel,Selector,SelectionKey),可是跟底层对应不起来啊,这一篇就透过源码看一下,大概能帮助你更好的了解这几个类,及底层的实现。偏重个人兴趣向整理,如有不适,欢迎吐槽

Linux网络编程

查阅资料的时候,发现wiki百科讲的已经十分好了,我先贴下原文Berkeley套接字,相当完美的描述了Socket相关Api介绍及demo演示,由于,大学学的c语言都快还给老师了,写个demo已然不太现实,这里就臭不要脸的套用wiki百科的demo,c语言讲解部分,如果有错误欢迎指出~~。

下面是精简版本的linux网络编程,详细版可以参考链接。

BIO

demo

  /* Server code in C */
     
  #include <sys/types.h>
  #include <sys/socket.h>
  #include <netinet/in.h>
  #include <arpa/inet.h>
  #include <stdio.h>
  #include <stdlib.h>
  #include <string.h>
  #include <unistd.h>
  
  int main(void)
  {
    struct sockaddr_in stSockAddr;
    //TODO 1 socket
    int SocketFD = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
  
    if(-1 == SocketFD)
    {
      perror("can not create socket");
      exit(EXIT_FAILURE);
    }
  
    memset(&stSockAddr, 0, sizeof(struct sockaddr_in));
  
    stSockAddr.sin_family = AF_INET;
    stSockAddr.sin_port = htons(1100);
    stSockAddr.sin_addr.s_addr = INADDR_ANY;
    //TODO 2  bind
    if(-1 == bind(SocketFD,(const struct sockaddr *)&stSockAddr, sizeof(struct sockaddr_in)))
    {
      perror("error bind failed");
      close(SocketFD);
      exit(EXIT_FAILURE);
    }
    //TODO 3  listen
    if(-1 == listen(SocketFD, 10))
    {
      perror("error listen failed");
      close(SocketFD);
      exit(EXIT_FAILURE);
    }
  
    for(;;)
    {
      //TODO 4 accept
      int ConnectFD = accept(SocketFD, NULL, NULL);
  
      if(0 > ConnectFD)
      {
        perror("error accept failed");
        close(SocketFD);
        exit(EXIT_FAILURE);
      }
  
     /* perform read write operations ... */
  
      shutdown(ConnectFD, SHUT_RDWR);
  
      close(ConnectFD);
    }

    close(SocketFD);
    return 0;
  }
 /* Client code in C */

  #include <sys/types.h>
  #include <sys/socket.h>
  #include <netinet/in.h>
  #include <arpa/inet.h>
  #include <stdio.h>
  #include <stdlib.h>
  #include <string.h>
  #include <unistd.h>
  
  int main(void)
  {
    struct sockaddr_in stSockAddr;
    int Res;
    int SocketFD = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
  
    if (-1 == SocketFD)
    {
      perror("cannot create socket");
      exit(EXIT_FAILURE);
    }
  
    memset(&stSockAddr, 0, sizeof(struct sockaddr_in));
  
    stSockAddr.sin_family = AF_INET;
    stSockAddr.sin_port = htons(1100);
    Res = inet_pton(AF_INET, "192.168.1.3", &stSockAddr.sin_addr);
  
    if (0 > Res)
    {
      perror("error: first parameter is not a valid address family");
      close(SocketFD);
      exit(EXIT_FAILURE);
    }
    else if (0 == Res)
    {
      perror("char string (second parameter does not contain valid ipaddress");
      close(SocketFD);
      exit(EXIT_FAILURE);
    }
    //TODO 5 connect
    if (-1 == connect(SocketFD, (const struct sockaddr *)&stSockAddr, sizeof(struct sockaddr_in)))
    {
      perror("connect failed");
      close(SocketFD);
      exit(EXIT_FAILURE);
    }
  
    /* perform read write operations ... */
  
    shutdown(SocketFD, SHUT_RDWR);
  
    close(SocketFD);
    return 0;
  }

LinuxAPI

socket()

  • socket() 创建一个新的确定类型的套接字,返回套接字。
    • api: int socket(int , int type, int protocol);
    • 示例:上文 TODO 1
    • 参数:
      • domain: 为创建的套接字指定协议集 eg. IPV4
      • type: socket类型 eg. 流,数据报文
      • protocol: 实际传输协议 eg. TCP,UDP

bind()

  • bind() 为一个套接字分配地址。当使用socket()创建套接字后,只赋予其所使用的协议,并未分配地址。在接受其它主机的连接前,必须先调用bind()为套接字分配一个地址。
    • api: int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);
    • 示例:参见上文 TODO 2
    • 参数:
      • sockfd:套接字描述符,上面返回的套接字
      • my_addr: 指向sockaddr结构(用于表示所分配地址)的指针
      • addrlen: 用socklen_t字段指定了sockaddr结构的长度

listen()

  • listen() 当socket和一个地址绑定之后,listen()函数会开始监听可能的连接请求。然而,这只能在有可靠数据流保证的时候使用,例如:数据类型(SOCK_STREAMSOCK_SEQPACKET)。

    • api:int listen(int sockfd, int backlog);
    • 示例:参见上文 TODO 3
    • 参数:
      • sockfd: 套接字描述符,上面返回的套接字
      • backlog: 完成三次握手、等待accept的全连接的队列的最大长度上限。

accept()

  • accept()当应用程序监听来自其他主机的面对数据流的连接时,通过事件(比如Unix select()系统调用)通知它。必须用 accept()函数初始化连接。 Accept() 为每个连接创立新的套接字并从监听队列中移除这个连接。它使用如下参数:
    • api:int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
    • 示例:参见上文 TODO 4
    • 参数:
      • sockfd:监听的套接字描述符
      • cliaddr: 指向sockaddr 结构体的指针,客户机地址信息。
      • addrlen:指向 socklen_t的指针,确定客户机地址结构体的大小 。

connect()

  • connect() 系统调用为一个套接字设置连接,参数有文件描述符和主机地址。链接到指定地址
    • api:int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
    • 示例:参见上文 TODO 5
    • 参数:
      • sockfd:监听的套接字描述符
      • serv_addr: 指向sockaddr 结构体的指针,服务器地址信息。
      • addrlen:指向 socklen_t的指针,确定服务器地址结构体的大小 。

select()

  • select():在一段指定的时间内,监听用户感兴趣的文件描述符上可读、可写和异常等事件
    • api:int select (int nfds, fd_set FAR * readfds, fd_set FAR * writefds, fd_set FAR * exceptfds, const struct timeval FAR * timeout);
    • 参数:
      • nfds:没有用,仅仅为与伯克利Socket兼容而提供。
      • readfds:指定一个Socket数组,select检查该数组中的所有Socket。如果成功返回,则readfds中存放的是符合‘可读性’条件的数组成员(如缓冲区中有可读的数据)。
      • writefds:指定一个Socket数组,select检查该数组中的所有Socket。如果成功返回,则writefds中存放的是符合‘可写性’条件的数组成员(包括连接成功)。
      • exceptfds:指定一个Socket数组,select检查该数组中的所有Socket。如果成功返回,则cxceptfds中存放的是符合‘有异常’条件的数组成员(包括连接接失败)。
      • timeout:指定select执行的最长时间,如果在timeout限定的时间内,readfds、writefds、exceptfds中指定的Socket没有一个符合要求,就返回0。

poll()

  • poll()用于检查套接字的状态。 套接字可以被测试,看是否可以写入、读取或是有错误。
    • api:int poll(struct pollfd * fds , nfds_t nfds , int timeout );
    • 参数:
      • fds是pollfd结构体指针
      • nfdsnfds是描述符个数,结构体pollfd数组元素的个数
      • timeout:参数设置为-1时,表示永远阻塞等待。0表示立即返回,不阻塞。大于0时,表示等待指定数目的毫秒数。

NIO

demo

#include<stdio.h>
#include<arpa/inet.h>
#include<sys/epoll.h>
#include<unistd.h>
#include<ctype.h>
#define MAXLEN 1024
#define SERV_PORT 8000
#define MAX_OPEN_FD 1024

int main(int argc,char *argv[])
{
    int  listenfd,connfd,efd,ret;
    char buf[MAXLEN];
    struct sockaddr_in cliaddr,servaddr;
    socklen_t clilen = sizeof(cliaddr);
    struct epoll_event tep,ep[MAX_OPEN_FD];

    listenfd = socket(AF_INET,SOCK_STREAM,0);
    //TODO 6 fcntl
    fcntl(listenfd, F_SETFL, fdflags | O_NONBLOCK);

    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(SERV_PORT);
    bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
    listen(listenfd,20);
    // 创建一个epoll fd
    //TODO 7 epoll_create
    efd = epoll_create(MAX_OPEN_FD);
    tep.events = EPOLLIN;tep.data.fd = listenfd;
    // 把监听socket 先添加到efd中
    //TODO 8 epoll_ctl
    ret = epoll_ctl(efd,EPOLL_CTL_ADD,listenfd,&tep);
    // 循环等待
    for (;;)
    {
        //TODO 9 epoll_wait
        // 返回已就绪的epoll_event,-1表示阻塞,没有就绪的epoll_event,将一直等待
        size_t nready = epoll_wait(efd,ep,MAX_OPEN_FD,-1);
        for (int i = 0; i < nready; ++i)
        {
            // 如果是新的连接,需要把新的socket添加到efd中
            if (ep[i].data.fd == listenfd )
            {
                connfd = accept(listenfd,(struct sockaddr*)&cliaddr,&clilen);
                tep.events = EPOLLIN;
                tep.data.fd = connfd;
                ret = epoll_ctl(efd,EPOLL_CTL_ADD,connfd,&tep);
            }
            // 否则,读取数据
            else
            {
                connfd = ep[i].data.fd;
                int bytes = read(connfd,buf,MAXLEN);
                // 客户端关闭连接
                if (bytes == 0){
                    ret =epoll_ctl(efd,EPOLL_CTL_DEL,connfd,NULL);
                    close(connfd);
                    printf("client[%d] closed\n", i);
                }
                else
                {
                    for (int j = 0; j < bytes; ++j)
                    {
                        buf[j] = toupper(buf[j]);
                    }
                    // 向客户端发送数据
                    write(connfd,buf,bytes);
                }
            }
        }
    }
    return 0;
}

fcntl()

  • fcntl()打开文件描述符,具体操作由cmd决定
    • api:int fcntl(int fd , int cmd , ... /* arg */ );
    • 示例:参见上文 TODO 6
    • 参数
      • fd:文件描述符
      • cmd:操作指令

epoll_create()

  • epoll_create()在内核中创建epoll实例并返回一个epoll文件描述符,结构体为eventpoll
    • api:int epoll_create(int size);
    • 示例:参见上文 TODO 7
    • 参数:
      • size:而现在 size 已经没有这种语义了,但是调用者调用时 size 依然必须大于 0,以保证后向兼容性。

epoll_ctl()

  • epoll_ctl()向 epfd 对应的内核epoll 实例添加、修改或删除对 fd 上事件 event 的监听。
    • api:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    • 示例:参见上文 TODO 8
    • 参数
      • epfdepoll结构体
      • opcud对应的事件枚举
      • fd文件描述符
      • events水平触发or边缘触发

epoll_wait()

  • epoll_wait()等待其管理的连接上的 IO 事件
    • api:int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
    • 示例:参见上文 TODO 9
    • 参数
      • epfdepoll结构体
      • eventsepoll_event结构体指针
      • maxevents最多返回多少事件
      • timeout当 timeout 为 0 时,epoll_wait 永远会立即返回。而 timeout 为 -1 时,epoll_wait 会一直阻塞直到任一已注册的事件变为就绪。当 timeout 为一正整数时,epoll 会阻塞直到计时 timeout 毫秒终了或已注册的事件变为就绪。

send()和recv(),或者write()和read(),或者recvfrom()和sendto()等

  • 用于往/从远程套接字发送和接受数据

相关api大致如上,如有更精细的可以自行搜索。

其他延伸

linux内核可以看腾讯大佬:张彦⻜的博客,这里就简述下,方便有个印象

  • epoll_create

会创建如下结构体

struct eventpoll {


   // sys_epoll_wait⽤到的等待队列 
   wait_queue_head_t wq;

   //接收就绪的描述符
   struct list_head rdllist;

   // 红⿊树,管理添加进来的socket(封装为epitem)
   struct rb_root rbr;
       
};
  • epoll_ctl

根据行为枚举的不同,进行不同的处理,对于EPOLL_CTL_ADD,会添封装成将socket封装成epitem,插入到红黑树中

  • epoll_wait

查询就绪队列,就绪队列中没有,会添加进epoll对象的等待队列中,让出线程,当有数据进入,会讲等待队列中对应的项,添加到epoll的就绪队列,并唤醒线程,继续执行剩余代码

参考文章

Berkeley套接字:zh.wikipedia.org/wiki/Berkel…

epoll:zh.wikipedia.org/wiki/Epoll

linux文档:man7.org/linux/man-p…