高级IO

作者: 飞翃荷兰人 | 来源:发表于2020-04-09 00:43 被阅读0次

本篇文章主要涉及的内容有阻塞和非阻塞IO,同步异步IO,还有网络套接字IO,包括select,poll,epoll这些多路复用等,希望能把自己理解的东西讲清楚。
首先不要对高级这个词有什么误区。高级并不是指它比之前的io要复杂得多,而是相对于底层来说,其处于一个更高的位置,离程序员更近,就像C语言之于汇编一样。

一 高级IO的几种类别

1 非阻塞IO

非阻塞IO并不是什么新的IO方式,它只是说系统在IO时,读写不到东西不会被阻塞,如果这种操作不能完成,则调用立即出错返回,表示该操作如继续执行将阻塞。对于一个给定的描述符,有两种为其指定非阻塞I/O的方法。

  • (1). 如果调用open获得描述符,则可指定O_NONBLOCK标志。
int open(const char *path, int oflag,... /* mode_t mode */);
// mode = O_NONBLOCK. 如果path引用的是一个FIFO、一个块特殊文件或一个字符特殊文件,则此选项为文件的本次打开操作和后续的I/O操作设置非阻塞方式。
  • (2). 对于已经打开的一个描述符,则可调用fcntl,由该函数打开 O_NONBLOCK 文件状态标志(见3.14节)。图3-12中的函数可用来为一个描述符打开任一文件状态标志。
int fcntl(int fd, int cmd, ... /* int arg */);
// F_SETFL 将文件状态标志设置为第3个参数的值(取为整型值)。可以更改的几个标志是:O_APPEND、O_NONBLOCK、O_SYNC、O_DSYNC、O_RSYNC、O_FSYNC和O_ASYNC。

2 IO多路复用

先看如下代码:

while ((n=read(STDIN_FILENO, buf, BUFSIZ)) > 0)
    if (write(STDOUT_FILENO, buf, n) != n)
        err_sys("write error");

上述代码从标准输入读,写到标准输出,代码在这里被阻塞了,如果要读两个文件描述符,后面只能等着。此时,用多进程,多线程,非阻塞这些手段都能解决问题,但是要么同步复杂,要么浪费资源,这时IO多路复用就闪亮登场了。多路复用让一个进程可以同时处理多个文件描述符,不会因为一个被阻塞而影响整体。

2.1 多路复用的函数

select函数

#include <sys/select.h>
int select(int maxfdp1, fd_set *restrict readfds,
               fd_set *restrict writefds, fd_set *restrict exceptfds,
               struct timeval *restrict tvptr);
//返回值:准备就绪的描述符数目;若超时,返回0;若出错,返回−1

先来说明最后一个参数,它指定愿意等待的时间长度,单位为秒和微秒。
tvptr == NULL
永远等待。如果捕捉到一个信号则中断此无限期等待。当所指定的描述符中的一个已准备好或捕捉到一个信号则返回。如果捕捉到一个信号,则select返回-1,errno设置为EINTR。
tvptr->tv_sec == 0 && tvptr->tv_usec == 0
根本不等待。测试所有指定的描述符并立即返回。这是轮询系统找到多个描述符状态而不阻塞select函数的方法。
tvptr->tv_sec != 0 || tvptr->tv_usec != 0
等待指定的秒数和微秒数。当指定的描述符之一已准备好,或当指定的时间值已经超过时立即返回。如果在超时到期时还没有一个描述符准备好,则返回值是 0。(如果系统不提供微秒级的精度,则tvptr->tv_usec值取整到最近的支持值。)与第一种情况一样,这种等待可被捕捉到的信号中断。

中间3个参数readfds、writefds和exceptfds是指向描述符集的指针。这3个描述符集说明了我们关心的可读、可写或处于异常条件的描述符集合。每个描述符集存储在一个fd_set数据类型中。这个数据类型是由实现选择的,它可以为每一个可能的描述符保持一位。(位图)
select的中间3个参数(指向描述符集的指针)中的任意一个(或全部)可以是空指针,这表示对相应条件并不关心。

select第一个参数maxfdp1的意思是“最大文件描述符编号值加1”。考虑所有3个描述符集,在3个描述符集中找出最大描述符编号值,然后加1,这就是第一个参数值。也可将第一个参数设置为FD_SETSIZE,这是<sys/select.h>中的一个常量,它指定最大描述符数(经常是1024),但是对大多数应用程序而言,此值太大了。确实,大多数应用程序只使用3~10个描述符(某些应用程序需要更多的描述符,但这种UNIX程序并不典型)。通过指定我们所关注的最大描述符,内核就只需在此范围内寻找打开的位,而不必在3个描述符集中的数百个没有使用的位内搜索。

从这就可以看出来,为什么select函数并不关心它中间三个参数的值,因为与其浪费时间去遍历这三个1024位的集合,不如就从头遍历到首个参数,会节省很多时间。

(1)返回值-1表示出错。这是可能发生的,例如,在所指定的描述符一个都没准备好时捕捉到一个信号。在此种情况下,一个描述符集都不修改。
(2)返回值0表示没有描述符准备好。若指定的描述符一个都没准备好,指定的时间就过了,那么就会发生这种情况。此时,所有描述符集都会置0。
(3)一个正返回值说明了已经准备好的描述符数。该值是3个描述符集中已准备好的描述符数之和,所以如果同一描述符已准备好读和写,那么在返回值中会对其计两次数。在这种情况下, 3个描述符集中仍旧打开的位对应于已准备好的描述符。

对于“准备好”的含义要作一些更具体的说明。
•若对读集(readfds)中的一个描述符进行的read操作不会阻塞,则认为此描述符是准备好的。
•若对写集(writefds)中的一个描述符进行的write操作不会阻塞,则认为此描述符是准备好的。
•若对异常条件集(exceptfds)中的一个描述符有一个未决异常条件,则认为此描述符是准备好的。现在,异常条件包括:在网络连接上到达带外的数据,或者在处于数据包模式的伪终端上发生了某些条件。(Stevens[1990]的15.10节中描述了后一种条件。)
•对于读、写和异常条件,普通文件的文件描述符总是返回准备好。

一个描述符阻塞与否并不影响select是否阻塞,理解这一点很重要。也就是说,如果希望读一个非阻塞描述符,并且以超时值为5秒调用select,则select最多阻塞5 s(因为有可能文件就绪,立马返回)。相类似,如果指定一个无限的超时值,则在该描述符数据准备好,或捕捉到一个信号之前,select会一直阻塞。换言之:select的阻塞是和自己的函数参数有关,和文件描述符阻塞时间无关。

poll函数

#include <poll.h>
int poll(struct pollfd fdarray[], nfds_t nfds, int timeout);
返回值:准备就绪的描述符数目;若超时,返回0;若出错,返回-1

//与select不同,poll不是为每个条件(可读性、可写性和异常条件)构造一个描述符集,
//而是构造一个pollfd结构的数组,每个数组元素指定一个描述符编号以及我们对该描述符感兴趣的条件。
struct pollfd {
    int  fd;    /* file descriptor to check, or < 0 to ignore */
    short events; /* events of interest on fd */
    short revents; /* events that occurred on fd */
};

可以将pollfd成员设置为下图的值:


image.png

返回时,revents 成员由内核设置,用于说明每个描述符发生了哪些事件。

poll的最后一个参数指定的是我们愿意等待多长时间。如同select一样,有3种不同的情形。

  • timeout == -1
    永远等待。(某些系统在<stropts.h>中定义了常量INFTIM,其值通常是-1。)当所指定的描述符中的一个已准备好,或捕捉到一个信号时返回。如果捕捉到一个信号,则poll返回-1,errno设置为EINTR。
  • timeout == 0
    不等待。测试所有描述符并立即返回。这是一种轮询系统的方法,可以找到多个描述符的状态而不阻塞poll函数。
  • timeout > 0
    等待timeout毫秒。当指定的描述符之一已准备好,或timeout到期时立即返回。如果timeout到期时还没有一个描述符准备好,则返回值是0。(如果系统不提供毫秒级精度,则timeout值取整到最近的支持值。)

epoll
epoll的使用主要是三个函数,关注点与poll有点类似,要关注文件描述符和事件。下面内容主要来源于:https://zhuanlan.zhihu.com/p/119400472,这篇内容讲的更好一些:https://www.zhihu.com/collection/389934235

  • epoll_create
asmlinkage long sys_epoll_create(int size)
{
    int error, fd;
    struct inode *inode;
    struct file *file;

    error = ep_getfd(&fd, &inode, &file);
    error = ep_file_init(file);

    return fd;
}

调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个 红黑树 用于存储以后epoll_ctl传来的socket外,还会再建立一个list链表,用于存储准备就绪的事件.

  • epoll_ctl
sys_epoll_ctl(int epfd, int op, int fd, struct epoll_event __user *event)
{
    int error;
    struct file *file, *tfile;
    struct eventpoll *ep;
    struct epitem *epi;
    struct epoll_event epds;

    error = -EFAULT;
    // 不是删除操作则复制用户数据到内核
    if (
        EP_OP_HASH_EVENT(op) &&
        copy_from_user(&epds, event, sizeof(struct epoll_event))
      )
        goto eexit_1;

    // 根据一种的图,拿到epoll对应的file结构体
    file = fget(epfd);

    // 拿到操作的文件的file结构体
    tfile = fget(fd);
    // 通过file拿到epoll_event结构体,见上面的图
    ep = file->private_data;
    // 看这个文件描述符是否已经存在,epoll用红黑树维护这个数据
    epi = ep_find(ep, tfile, fd);

    switch (op) {
    // 新增
    case EPOLL_CTL_ADD:
        // 还没有则新增,有则报错
        if (!epi) {
            epds.events |= POLLERR | POLLHUP;
            // 插入红黑树
            error = ep_insert(ep, &epds, tfile, fd);
        } else
            error = -EEXIST;
        break;
    // 删除
    case EPOLL_CTL_DEL:
        // 存在则删除,否则报错
        if (epi)
            error = ep_remove(ep, epi);
        else
            error = -ENOENT;
        break;
    // 修改
    case EPOLL_CTL_MOD:
        // 存在则修改,否则报错
        if (epi) {
            epds.events |= POLLERR | POLLHUP;
            error = ep_modify(ep, epi, &epds);
        } else
            error = -ENOENT;
        break;

当我们执行epoll_ctl时,除了把socket放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。所以,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后就来把socket插入到准备就绪链表里了。

  • epoll_wait
asmlinkage long sys_epoll_wait(int epfd, struct epoll_event __user *events,
                   int maxevents, int timeout)
{
    int error;
    struct file *file;
    struct eventpoll *ep;
    // 通过epoll的fd拿到对应的file结构体
    file = fget(epfd);
    // 通过file结构体拿到eventpoll结构体
    ep = file->private_data;
    error = ep_poll(ep, events, maxevents, timeout);
    return error;
}

static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events,
           int maxevents, long timeout)
{
    int res, eavail;
    unsigned long flags;
    long jtimeout;
    wait_queue_t wait;

    // 计算超时时间
    jtimeout = timeout == -1 || timeout > (MAX_SCHEDULE_TIMEOUT - 1000) / HZ ?
        MAX_SCHEDULE_TIMEOUT: (timeout * HZ + 999) / 1000;

retry:
    res = 0;
    // 就绪队列为空
    if (list_empty(&ep->rdllist)) {
        // 加入阻塞队列
        init_waitqueue_entry(&wait, current);
        add_wait_queue(&ep->wq, &wait);

        for (;;) {
            // 挂起
            set_current_state(TASK_INTERRUPTIBLE);
            // 超时或者有就绪事件了,则跳出返回
            if (!list_empty(&ep->rdllist) || !jtimeout)
                break;
            // 被信号唤醒返回EINTR
            if (signal_pending(current)) {
                res = -EINTR;
                break;
            }

            // 设置定时器,然后进程挂起,等待超时唤醒(超时或者信号唤醒)
            jtimeout = schedule_timeout(jtimeout);
        }
        // 移出阻塞队列
        remove_wait_queue(&ep->wq, &wait);
        // 设置就绪
        set_current_state(TASK_RUNNING);
    }

    // 是否有事件就绪,唤醒的原因有几个,被唤醒不代表就有就绪事件
    eavail = !list_empty(&ep->rdllist);

    write_unlock_irqrestore(&ep->lock, flags);
    // 处理就绪事件返回
    if (!res && eavail &&
        !(res = ep_events_transfer(ep, events, maxevents)) && jtimeout)
        goto retry;

    return res;
}

当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。而且,通常情况下即使我们要监控百万计的句柄,大多一次也只返回很少量的准备就绪句柄而已,所以,epoll_wait仅需要从内核态copy少量的句柄到用户态而已.

epoll的用法:

#define MAX_EVENTS 10
           struct epoll_event ev, events[MAX_EVENTS];
           int listen_sock, conn_sock, nfds, epollfd;

           /* Code to set up listening socket, 'listen_sock',
              (socket(), bind(), listen()) omitted */

           // 创建epoll实例
           epollfd = epoll_create1(0);

           if (epollfd == -1) {
               perror("epoll_create1");
               exit(EXIT_FAILURE);
           }

           // 将监听的端口的socket对应的文件描述符添加到epoll事件列表中
           ev.events = EPOLLIN;
           ev.data.fd = listen_sock;
           if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
               perror("epoll_ctl: listen_sock");
               exit(EXIT_FAILURE);
           }

           for (;;) {
               // epoll_wait 阻塞线程,等待事件发生
               nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
               if (nfds == -1) {
                   perror("epoll_wait");
                   exit(EXIT_FAILURE);
               }

               for (n = 0; n < nfds; ++n) {
                   if (events[n].data.fd == listen_sock) {
                       // 新建的连接
                       conn_sock = accept(listen_sock,
                                          (struct sockaddr *) &addr, &addrlen);
                       // accept 返回新建连接的文件描述符
                       if (conn_sock == -1) {
                           perror("accept");
                           exit(EXIT_FAILURE);
                       }
                       setnonblocking(conn_sock);
                       // setnotblocking 将该文件描述符置为非阻塞状态

                       ev.events = EPOLLIN | EPOLLET;
                       ev.data.fd = conn_sock;
                       // 将该文件描述符添加到epoll事件监听的列表中,使用ET模式
                       if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock,
                                   &ev) == -1)
                           perror("epoll_ctl: conn_sock");
                           exit(EXIT_FAILURE);
                       }
                   } else {
                       // 使用已监听的文件描述符中的数据
                       do_use_fd(events[n].data.fd);
                   }
               }
           }

要注意一点:不是所有文件都能用epoll的,普通文件不能用epoll,普通文件为什么不能用epoll

  • 水平触发:文件就绪后,如果一次没读完,epoll_wait会再次返回epoll_in,或者epoll_out,可以接着读。
  • 边缘触发:epoll_wait只返回一次,要一次读完,要不然就下次再读。

对epoll做一个小节:
最后对epoll作一个小结,epoll主要是有三个函数,epoll_create,epoll_ctl和epoll_wait; epoll_create创建了一个epoll所需要的基本信息,包括一个监听文件描述符的红黑树,一个就绪链表存放就绪的文件描述符。epoll的高效之处:一个在于它的callback机制啊,在文件就绪之后,它会有一个callback函数把就绪文件添加到就就绪链表,同时唤醒wait。还有就是因为它只在内核和用户态之间传递这些就绪的文件描述符,所以IO开销会比较小。epoll在有大规模请求的时候作用优势明显,如果监听较少,效率不一定有select高。

selct poll epoll区别和联系

select 是一种较早出现的IO多路复用的方式,他通过指定一个最大的文件描述符遍历的方式扫描就绪事件,如果有已完成的事件就返回,他的主要问题有两点:

  • (1) 在于它的返回太多了,为读,写,异常,三种每一个都有单独的set(位模式),如果一个文件读写同时发生就出现两次,一定程度上造成了内核态和用户态之间的IO的低效率。
  • (2) 它的set一般被设为1024,有可能会更多或更少,限制了它所能监听的事件的个数。
  • (3) 需要依次轮训set的各个位(bit)。

poll相对于select而言,它用一个pollfd数组来装要监听的文件描述符,所以没有了监听个数的限制,同时为每个文件描述符制定了属性[可以多个属性 (读/写/异常)],轮训完成后设置revents值哪些文件已就绪,减少了一部分内核态拷贝到用户态的IO开销,但是仍然需要轮训的方式拿到就绪文件。

epoll 相对于poll更有进步,用一个红黑树装所有的监听文件,在判断是否就绪时,不通过遍历,而是通过内核callback的形式,精确找到就绪文件,同时用一个链表装所有的就绪文件,在返回就绪时,较少拷贝开销。

2 异步IO

关于描述符的状态,系统并不主动告诉我们任何信息,我们需要进行查询(调用select或poll)。信号机构可以提供了一种以异步形式通知某种事件已发生的方法:使用一个信号通知进程,对某个描述符所关心的某个事件已经发生。但这些形式的异步I/O是受限制的:它们并不能用在所有的文件类型上,而且只能使用一个信号。如果要对一个以上的描述符进行异步I/O,那么在进程接收到该信号时并不知道这一信号对应于哪一个描述符。

在我们了解使用异步I/O的不同方法之前,需要先讨论一下成本。在用异步I/O的时候,要通过选择来灵活处理多个并发操作,这会使应用程序的设计复杂化。更简单的做法可能是使用多线程,使用同步模型来编写程序,并让这些线程以异步的方式运行。
使用POSIX异步I/O接口,会带来下列麻烦。
•每个异步操作有 3 处可能产生错误的地方:一处在操作提交的部分,一处在操作本身的结果,还有一处在用于决定异步操作状态的函数中。
•与POSIX异步I/O接口的传统方法相比,它们本身涉及大量的额外设置和处理规则。

存储映射I/O

存储映射I/O(memory-mapped I/O)能将一个磁盘文件映射到存储空间中的一个缓冲区上,于是,当从缓冲区中取数据时,就相当于读文件中的相应字节。与此类似,将数据存入缓冲区时,相应字节就自动写入文件。这样,就可以在不使用read和write的情况下执行I/O。

为了使用这种功能,应首先告诉内核将一个给定的文件映射到一个存储区域中。这是由mmap函数实现的。

#include <sys/mman.h>
void *mmap(void *addr, size_t len, int prot, int flag, int fd, off_t off);
// 返回值:若成功,返回映射区的起始地址;若出错,返回MAP_FAILED

addr参数用于指定映射存储区的起始地址。通常将其设置为0,这表示由系统选择该映射区的起始地址。此函数的返回值是该映射区的起始地址。
fd参数是指定要被映射文件的描述符。在文件映射到地址空间之前,必须先打开该文件。len参数是映射的字节数,off是要映射字节在文件中的起始偏移量(有关off值的一些限制将在后面说明)。
一个标准流程:

程序首先打开两个文件,然后调用fstat得到输入文件的长度。在为输入文件调用mmap和设置输出文件长度时都需使用输入文件长度。可以调用ftruncate设置输出文件的长度。如果不设置输出文件的长度,则对输出文件调用mmap也可以,但是对相关存储区的第一次引用会产生SIGBUS信号。
然后对每个文件调用mmap,将文件映射到内存,最后调用memcpy将输入缓冲区的内容复制到输出缓冲区。为了限制使用内存的量,我们每次最多复制 1 GB 的数据(如果系统没有足够的内存,可能无法把一个很大的文件中的所有内容都映射到内存中)。在映射文件中的后一部分数据之前,我们需要解除前一部分数据的映射。
在从输入缓冲区(src)取数据字节时,内核自动读输入文件;在将数据存入输出缓冲区(dst)时,内核自动将数据写到输出文件中。

read/write 和 mmap/memcpy效率对比:


image.png

二者的主要区别在于,与mmap和memcpy相比,read和write执行了更多的系统调用,并做了更多的复制。read和write将数据从内核缓冲区中复制到应用缓冲区(read),然后再把数据从应用缓冲区复制到内核缓冲区(write)。而mmap和memcpy则直接把数据从映射到地址空间的一个内核缓冲区复制到另一个内核缓冲区。当引用尚不存在的内存页时,这样的复制过程就会作为处理页错误的结果而出现(每次错页读发生一次错误,每次错页写发生一次错误)。如果系统调用和额外的复制操作的开销和页错误的开销不同,那么这两种方法中就会有一种比另一种表现更好。

在Linux 3.2.0中,相对于运行时间,两种版本的程序在时钟时间上显示出了巨大的差异:使用read和write的版本完成任务比使用mmap和memcpy的版本快了4倍。然而在Solaris 10中,使用mmap和memcpy的版本比使用read和write的版本要快。既然二者的CPU时间几乎是相同的,为何它们的时钟时间差异却如此之大呢?一种可能是,在一种版本中需要较长的时间来等待I/O完成。这个等待时间并没有计算在CPU的处理时间中。另一种可能是,某些系统处理的时间可能并没有在程序中计算,比如系统守护进程把页写到磁盘中的操作。由于需要为读和写分配页,系统的守护进程会帮助我们准备可用的页。如果页的写操作是随机的而非连续的,那么把它们写入磁盘所需要的时间会更长,因此在页可以被用来复用之前所需等待的时间也会更长。

总结: mmap比read/write减少了系统调用次数,理论上应该数据会好看一些,但是可能系统没有把read/write的IO等待时间算进CPU耗时,导致了数据的差异。也有可能是内存的页错误导致的。

套接字

终于到了网络编程的重点:套接字,互联网开发日常接触最多的IO。
首先看一下创建套接字的api:

#include <sys/socket.h>
int socket (int domain, int type, int protocol);
返回值:若成功,返回文件(套接字)描述符;若出错,返回−1

domain:

image.png

type:

image.png

protocol:

image.png

虽然套接字描述符本质上是一个文件描述符,但不是所有参数为文件描述符的函数都可以接受套接字描述符。图16-4总结了到目前为止所讨论的大多数以文件描述符为参数的函数使用套接字描述符时的行为。未指定和由实现定义的行为通常意味着该函数对套接字描述符无效。例如, lseek不能以套接字描述符为参数,因为套接字不支持文件偏移量的概念。

网络协议指定了字节序,因此异构计算机系统能够交换协议信息而不会被字节序所混淆。TCP/IP协议栈使用大端字节序。应用程序交换格式化数据时,字节序问题就会出现。对于TCP/IP,地址用网络字节序来表示,所以应用程序有时需要在处理器的字节序与网络字节序之间转换它们。例如,以一种易读的形式打印一个地址时,这种转换很常见。

全篇总结:

本文大部分是原封不动的复制了unix高级编程的一些内容,还有少部分是自己的理解,这部分内容翻译的很经典,实在想不出来,用更好的语言来描述。后面的套接字unix编程讲的不好,应该从网络编程来讲,但是内容太多了,以后有机会再写吧。

相关文章

  • 高级IO

    非阻塞IO,记录锁,系统V流机制,IO多路转接,readv和writev存储映射IO(mmap) pipe/soc...

  • 高级IO

    本篇文章主要涉及的内容有阻塞和非阻塞IO,同步异步IO,还有网络套接字IO,包括select,poll,epoll...

  • Linux 高级IO

    [TOC] Linux 高级IO 涉及到一些IO的高级用法 文件描述符重定向 dup 函数从当前可用的文件描述符中...

  • 高级io(三)

    2016-03-06 流ioctl操作 之前提到过ioctl函数,它能做其他io函数不能处理的事情。流系统中继续采...

  • 高级io(一)

    2016-03-01 非阻塞io 系统调用分为低速调用系统和其他,低速系统调用时可能会使进程永远阻塞的一类系统调用...

  • 高级io(二)

    2016-03-02 流 流是系统V提供的构造内核设备驱动程序和网络协议包的一种通用方法流在用户进程和设备驱动程序...

  • linux高级环境编程-高级IO

    本文主要理清非阻塞IO,记录锁,IO多路转接,异步IO,readv和writev函数以及存储映射IO。学习 1、同...

  • 标准io和文件io,套接字高级io

    概念 文件I/O称之为不带缓存的IO(unbuffered I/O)。不带缓存指的是每个read,write都调用...

  • JAVA高级面试——IO

    一.基础知识 IP地址和端口号 (1).一个通信实体不能有两个通信程序使用同一个端口号 一个端口号只能有一个通信实...

  • Java高级- IO流

    13.1.File类的使用 File类的使用1.File类的一个对象,代表一个文件或一个文件目录(俗称: 文件夹)...

网友评论

      本文标题:高级IO

      本文链接:https://www.haomeiwen.com/subject/mqtfmhtx.html