网络编程
1. 多线程共享一个socket:一个线程close socket其他线程还能继续读写吗?影响写,不影响读,四次挥手。shutdown和close的区别。
2. 往关闭的socket里写会怎么样:首次写会返回RST,read=0后,再写需要处理EPIPE错误,并忽视SIGPIPE信号。
3. 数据串行化,非阻塞模式下如何读取完指定的字节数?while退出条件?使用poll在超时时间内判断fd是否可读,while循环用于读取指定长度的字符串,如果只使用while循环会出现问题:始终读不全指定的字节数,但是客户端也不关闭socket,while会一直循环,所以需要在循环内判断是否读取超时,为了在超时时间内不浪费CPU,使用poll多路复用的方式来监听fd。
4. FIN与RST信号区别:RST会立刻清空缓存,而且不需要返回ACK。FIN默认是socket close后发送的信号,是否发送缓存需要对close进行配置。
5. 服务器间监听到大量的time_wait(主动关闭):服务器耗尽65535个端口后会拒绝客户端请求。
设置为长连接?请求RPC是随机获取一个RPCserver,设置长连接可以吗?设置长连接下次请求会不会换成其他server实例,而且长短连接是由业务决定的,处理时间远小于time_wait时间都是短链接。
设置可重用?短链接使用的端口是服务器临时分配的,客户端只指定了服务端的端口,不指定客户端端口,不能使用SO_REUSEADDR来设置可充用。有个问题而且设置可重用,在复用端口时,由于服务端没有收到ACK信号于是重发了FIN,会导致刚建立的连接被误中断。
对于大量短链接的业务,只能采用负载均衡的方式,可以配置time_wait回收速度来减少。
1. poll用法
int poll(struct pollfd fds[], nfds_t nfds, int timeout);
每隔timeout时间返回一次
参数说明:
fds:是一个struct pollfd结构类型的数组,用于存放需要检测其状态的Socket描述符;每当调用这个函数之后,系统不会清空这个数组,操作起来比较方便;特别是对于 socket连接比较多的情况下,在一定程度上可以提高处理的效率;这一点与select()函数不同,调用select()函数之后,select() 函数会清空它所检测的socket描述符集合,导致每次调用select()之前都必须把socket描述符重新加入到待检测的集合中(FD_SET);因 此,select()函数适合于只检测一个socket描述符的情况,而poll()函数适合于大量socket描述符的情况;
2.select用法
https://www.cnblogs.com/skyfsm/p/7079458.html
2.write与send recv与read区别
UDP通信
revefrom sendto 用于UDP通信中
TCP通信:
write read 有三个参数
write(int fd, const void*buf,size_t nbytes); read(int fd,void *buf,size_t nbyte)
read使用:每read一次需要根据返回的长度决定是否再次读。 数据在不超过指定的长度的时候有多少读多少,没有数据则会一直等待。所以一般情况下:我们读取数据都需要采用循环读的方式读取数据,因为一次read 完毕不能保证读到我们需要长度的数据,read 完一次需要判断读到的数据长度再决定是否还需要再次读取。
while (nleft > 0) { again: nread = ul_reado_tv(fd, ptr, nleft, tp);//ul_reado_tv内部使用pull的超时特性来判断是否有数据可读,使用非阻塞读在指定时间内完成数据的全部读取。在超时
//时间内会读取所有数据,超过指定时间会返回。也就是不使用阻塞读,方式读取超时返回。使用非阻塞读,外加poll来
//判断socket是否可读。1、读取到数据立刻返回 2、时间到了没有读取到数据就返回
//select、poll_wait、epoll_wait返回可读≠read去读的时候能读到,读采用了poll的方式在超时时间内去读 if (nread < 0) { if ((nread == -1) && (errno == EINTR)) { goto again; } else if (nread == -2) { errno = ETIMEDOUT; return -1; } else if (nread == -3) { errno = EIO; return -1; } else { return -1; } } else if (nread == 0) { break; } else { ptr += nread; nleft -= nread; } }
wite同样,一次写不能保证把指定的长度都写进去,需要while循环,来判断。
while (nleft > 0) { n = ul_writeo_tv(fd, ptr, (size_t)nleft, tp); if ((n == -1) && (errno == EINTR)) { continue; } if (n <= 0) { if (n == -2) { errno = ETIMEDOUT; } if (!(sockflag & O_NONBLOCK)) { ul_setsocktoblock(fd); } return -1; } nleft -= n; ptr += n; }
recv send 有四个参数
int recv(int sockfd,void *buf,int len,int flags) int send(int sockfd,void *buf,int len,int flags)
第四个参数作用:
MSG_PEEK : 查看数据,并不从系统缓冲区移走数据,recv使用
recv()的原型是ssize_t recv(int sockfd, void *buf, size_t len, int flags);
我们在编写函数时,通常flags设置为0,此时recv()函数读取tcp 缓冲区中的数据到buf中,并从tcp 缓冲区中移除已读取的数据。如果把flags设置为MSG_PEEK,仅仅是把tcp 缓冲区中的数据读取到buf中,没有把已读取的数据从tcp 缓冲区中移除,如果再次调用recv()函数仍然可以读到刚才读到的数据
MSG_WAITALL : 阻塞读,recv使用。等待直到读取到buff_size 长度的数据,但是有可能中断引起返回,数据却没有读取到指定长度。
MSG_NOSIGNAL: MSG_NOSIGNAL,禁止 send或者recv函数向系统发送异常信号
3. send recv 发送过程
client发送数据到server,那么就是客户端进程调用send发送数据,而send的作用是将数据拷贝进入socket的内核发送缓冲区之中,然后send便会在上层返回。send()方法返回之时,数据不一定会 发送到对端即服务器上去。send()仅仅是把应用层buffer的数据拷贝进socket的内核发送buffer中,发送是TCP的事情,和send其实没有太大关系。
接收缓冲区把数据缓存入内核,等待recv()读取,recv()所做的工作,就是把内核缓冲区中的数据拷贝到应用层用户的buffer里面,并返回。若应用进程一直没有调用recv()进行读取的话,此数据会一直缓存在相应socket的接收缓冲区内。对于TCP,如果应用进程一直没有读取,接收缓冲区满了之后,发生的动作是:收端通知发端,接收窗口关闭(win=0)。这个便是滑动窗口的实现。保证TCP套接口接收缓冲区不会溢出,从而保证了TCP是可靠传输。因为对方不允许发出超过所通告窗口大小的数据。 这就是TCP的流量控制,如果对方无视窗口大小而发出了超过窗口大小的数据,则接收方TCP将丢弃它。
已经发送到网络的数据依然需要暂存在send buffer中,只有收到对方的ack后,kernel才从buffer中清除这一部分数据,为后续发送数据腾出空间。
接收端将收到的数据暂存在receive buffer中,自动进行确认。但如果socket所在的进程不及时将数据从receive buffer中取出,最终导致receive buffer填满,由于TCP的滑动窗口和拥塞控制,接收端会阻止发送端向其发送数据。这些控制皆发生在TCP/IP栈中,对应用程序是透明的,应用程序继续发送数据,最终导致send buffer填满,write调用阻塞。
一般来说,由于接收端进程从socket读数据的速度跟不上发送端进程向socket写数据的速度,最终导致发送端write调用阻塞。
5.
1. read总是在接收缓冲区有数据时立即返回,而不是等到给定的read buffer填满时返回。
只有当receive buffer为空时,
blocking模式才会等待,
nonblock模式下会立即返回-1(errno = EAGAIN或EWOULDBLOCK, 两个概念相同)
以 O_NONBLOCK的标志打开文件/socket/FIFO,如果你连续做read操作而没有数据可读。此时程序不会阻塞起来等待数据准备就绪返 回,read函数会返回一个错误EAGAIN,提示你的应用程序现在没有数据可读请稍后再试。
2. blocking的write只有在缓冲区足以放下整个buffer时才返回(与blocking read并不相同)
nonblock write则是返回能够放下的字节数,之后调用则返回-1(errno = EAGAIN或EWOULDBLOCK)
对于blocking的write有个特例:当write正阻塞等待时对面关闭了socket,则write则会立即将剩余缓冲区填满并返回所写的字节数,再次调用则write失败(connection reset by peer),这正是下个小节要提到的:
6.错误处理:
close socket 时会发送FIN,程序死掉,由系统代发FIN。
a与b进程进行TCP通信,b进程是异常终止的,发送FIN包是OS代劳的,b进程已经不复存在,当机器再次收到该socket的消息时,会回应RST(因为拥有该socket的进程已经终止)。a进程对收到RST的socket调用write时,操作系统会给a进程发送SIGPIPE,默认处理动作是终止进程。
7. read返回0,表示读取完毕。
8.SIGPIPE 往关闭的socket里面写数据
在网络编程中,SIGPIPE这个信号是很常见的。当往一个关闭的管道或socket连接中连续写入数据时会引发SIGPIPE信号,引发SIGPIPE信号的写操作将设置errno为EPIPE。在TCP通信中,当通信的双方中的一方close一个连接时,若另一方接着发数据,根据TCP协议的规定,会收到一个RST响应报文,若再往这个服务器发送数据时,系统会发出一个SIGPIPE信号给进程,告诉进程这个连接已经断开了,不能再写入数据。
SIGPIPE信号的默认行为是结束进程,而我们绝对不希望因为写操作的错误而导致程序退出,尤其是作为服务器程序来说就更恶劣了。所以我们应该对这种信号加以处理。
两种处理SIGPIPE信号的方式:
1. 给SIGPIPE设置SIG_IGN信号处理函数,忽略该信号:
signal(SIGPIPE, SIG_IGN); //忽视该信号,防止往关闭的socket写入消息导致程序崩溃
引发SIGPIPE信号的写操作将设置errno为EPIPE,。所以,第二次往关闭的socket中写入数据时, 会返回-1, 同时errno置为EPIPE. 这样,便能知道对端已经关闭,然后进行相应处理,而不会导致整个进程退出。第一次写返回RST报文,fd可读,读取值为0,但是写操作会继续进行,通过第二次写操作返回的错误值EPIPE来决定后续操作。
2. 使用send函数的MSG_NOSIGNAL 标志来禁止写操作触发SIGPIPE信号。
send(sockfd , buf , size , MSG_NOSIGNAL);
9.socket shutdown和close的区别
-
shutdown是可以单方向或者双方向关闭socket的方法。 而close则立即双方向关闭socket并释放相关资源。
-
如果有多个进程/线程共享一个socket,shutdown影响所有进程/线程,而close只影响本进程/线程。
-
shutdown() 立即关闭socket,并可以用来唤醒等待线程,如果其他线程阻塞在recv会立即返回。
-
close() 不一定立即关闭socket(如果有人引用, 要等到引用解除),不会唤醒等待线程。如果有其他线程阻塞在recv,不会换行该线程,该线程可以继续recv消息。
现在大部分网络应用都使用nonblocking socket和事件模型如epoll的时候, 因为nonblocking所以没有线程阻塞, 上面提到的行为差别不会体现出来
通过参数设置不同,调用close会出现如下A,B两种情况:
close:
A. 向客户端发送一个RST报文,丢弃本地缓冲区的未读数据,关闭socket并释放相关资源,此种方式为强制关闭。(l_onoff为非0,l_linger为0,)
B. 会继续发送缓冲区中的内容,发送完成再向客户端发送一个FIN报文,收到client端FIN ACK后,进入了FIN_WAIT_2阶段,可参考TCP四次挥手过程,此种方式为优雅关闭。如果在l_linger的时间内仍未完成四次挥手,则强制关闭。( l_onoff 为非0,l_linger为非0)
C.默认情况下:立即返回,如果发送缓冲区的内容会继续发送,发送完再返回。(l_onoff为0)
一端发送rst或者FIN,另一端都会read=0;
FIN与RST
RST表示复位,用来表示异常的关闭连接,在TCP的设计中它是不可或缺的。发送RST包关闭连接时,不必等缓冲区的包都发出去,直接就丢弃缓存区的包发送RST包。而接收端收到RST包后,也不必发送ACK包来确认。
FIN表示正常关闭连接,没有数据丢失,缓冲区所有数据包都发送完成才会发送FIN包,这与RST不同。
-
若server端发送FIN报文后没有收到client端的FIN ACK,会两次重传FIN报文,若一直收不到client端的FIN ACK,则会给client端发送RST信号,关闭socket并释放资源。(不同系统实现可能会不同)
-
client收到FIN信号后,再调用read函数会返回0。因为FIN的接收,表明client端以后再无数据可以接收,对方发来FIN,表明对方不在发送数据了。
-
close把描述符计数减一,等于0时,内核自动发送fin,对端接收到fin,如果还发送数据会被响应一个rst,如果还继续发送内核会给一个sigpipe信号,进程默认会退出。
接受到fin时,内核会先发送一个ack,进去close_wait,如果调用进程有close或者shutdown,会发送一个fin,进去last_ack状态,接受到最后一个ack就关闭了。
主动端发送fin后,这个套接字就被不能再用write和read了,在应用进程层面就是销毁的意义了,剩下的工作内核会自动完成。
client端如何知道已经接收到RST报文?
server发送RST报文后,并不等待从client端接收任何ack响应,直接关闭socket。而client端收到RST报文后,也不会产生任何响应。client端收到RST报文后,程序行为如下:
- 阻塞模型下,内核无法主动通知应用层出错,只有应用层主动调用read()或者write()这样的IO系统调用时,内核才会利用出错来通知应用层对端已经发送RST报文。
- 非阻塞模型下,select或者epoll会返回sockfd可读,应用层对其进行读取时,read()会报RST错误。
通过read write函数出错返回后,获取errno来确定对端是否发送RST信号。
client收到RST报文后应如何处理?
client端收到RST信号后,如果调用read函数读取,则会返回RST错误。在已经产生RST错误的情况下,继续调用write,则会发生epipe错误。此时内核将向客户进程发送 SIGPIPE 信号,该信号默认会使进程终止,通常程序会异常退出(未处理SIGPIPE信号的情况下)。
10.对一个已经收到FIN包的socket调用read方法
为了避免进程退出, 可以捕获SIGPIPE信号, 或者忽略它, 给它设置SIG_IGN信号处理函数:
signal(SIGPIPE, SIG_IGN);
这样, 第二次调用write方法时, 会返回-1, 同时errno置为SIGPIPE. 程序便能知道对端已经关闭.
11.EPOLL
EPOLLIN:有新连接请求,对端发送普通数据 或者对短关闭socket 触发EPOLLIN
对端正常关闭,触发EPOLLIN和EPOLLHUP
对端异常断开连接(只测了拔网线),没触发任何事件
EPOLLOUT 有数据要写
EPOLLERR 只有采取动作时,才能知道是否对方异常。比如读写已经关闭的socket会触发事件EPOLLERR事件
监听的fd,此fd的设置等待事件:EPOLLET | EPOLLIN (默认是LT模式,即EPOLLLT |EPOLLIN)
12.为什么多路复用会设置非阻塞读。
避免阻塞在read和write上。使用select和epoll需要阻塞在这两个函数上。
select、poll_wait、epoll_wait返回可读≠read去读的时候能读到。如果不用非阻塞,程序会永远卡在read或者accept上,导致不能返回select或者epoll
假如socket的读缓冲区已经有足够多的数据,需要read多次才能读完,如果是非阻塞可以在循环里读取,不用担心阻塞在read上,等到errno被置为EWOULDBLOCK的时候break,安全返回select。但如果是阻塞IO,只敢读取一次,因为如果读取没有数据的fd,read会阻塞,无法返回select。
13.网络编程中应用层buffer的作用
首先,multiplex的核心思想是——用一个线程去同时对多个socket连接服务,而想要做到这一点,thread/process就不能阻塞在某一个socket的read或write上,所以就要用到非阻塞IO原因见上。那么现在假设你要向一个socket发送100kb的数据,但是write调用中,操作系统只接受了80kb的数据,原因可能是受制于TCP的流量控制等等,现在你有两个选择:
1.等,你可以while这个write调用,但你不知道要等多久,这取决于对方什么时候收到之前的报文并且滑动窗口,而且这样也浪费了处理别的socket的时间。
2.把剩下的20kb存起来,下次再发,具体一点就是把这20kb保存在这个TCPconnection的output buffer里,并且注册EPOLLOUT事件,这样select下次返回的时候就还会来发送这20kb的数据,也不会影响别的socket的监听。
accept:当前没有可用的客户端连接请求,则会返回-1(errno设置为EAGAIN)
connect:客户端socket描述符为非阻塞模式,则调用connect之后,如果连接未能立即建立,则返回-1(errno设置为EINPROGRESS),如果select返回非阻塞socket描述符可写,则表明连接建立成功;如果select返回非阻塞socket描述符既可读又可写,需要判断连接是否出错。
14.fcntl函数有5种功能:
1. 复制一个现有的描述符(cmd=F_DUPFD).
2. 获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).
3. 获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).
4. 获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).
5. 获得/设置记录锁(cmd=F_GETLK , F_SETLK或F_SETLKW).
用法:
if ((flags = fcntl(sfd, F_GETFL, 0)) < 0 || fcntl(sfd, F_SETFL, flags | O_NONBLOCK) < 0){
}
15. 在Unix网络编程中通常用到setsockopt两个函数来获取和设置套接口的选项。
getsockopt()函数用于获取任意类型、任意状态套接口的选项当前值,并把结果存入optval。
1 #include <sys/socket.h>
2 int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
3 /*
4 sockfd:一个标识套接口的描述字。
5 level:选项定义的层次。例如,支持的层次有SOL_SOCKET、IPPROTO_TCP等。
6 optname:需获取的套接口选项。
7 optval:指针,指向存放所获得选项值的缓冲区。
8 optlen:指针,指向optval缓冲区的长度值。
9 */
setsockopt()函数用于任意类型、任意状态套接口的设置选项值。尽管在不同协议层上存在选项,但本函数仅定义了最高的“套接口”层次上的选项。
1 #include <sys/socket.h>
2 int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
3 /*
4 sockfd:标识一个套接口的描述字。
5 level:选项定义的层次;支持SOL_SOCKET、IPPROTO_TCP、IPPROTO_IP和IPPROTO_IPV6等。
6 optname:需设置的选项。
7 optval:指针,指向存放选项值的缓冲区。
8 optlen:optval缓冲区长度。
9 */
16.SO_LINGER作用
设置函数close()关闭TCP连接时的行为。缺省close()的行为是,如果有数据残留在socket发送缓冲区中则系统将继续发送这些数据给对方,等待被确认,然后返回。
利用此选项,可以将此缺省行为设置为以下两种
a.立即关闭该连接,通过发送RST分组(而不是用正常的FIN|ACK|FIN|ACK四个分组)来关闭该连接。至于发送缓冲区中如果有未发送完的数据,则丢弃。主动关闭一方的TCP状态则跳过TIMEWAIT,直接进入CLOSED。网上很多人想利用这一点来解决服务器上出现大量的TIMEWAIT状态的socket的问题,但是,这并不是一个好主意,这种关闭方式的用途并不在这儿,实际用途在于服务器在应用层的需求。
b.将连接的关闭设置一个超时。如果socket发送缓冲区中仍残留数据,进程进入睡眠,内核进入定时状态去尽量去发送这些数据。
在超时之前,如果所有数据都发送完且被对方确认,内核用正常的FIN|ACK|FIN|ACK四个分组来关闭该连接,close()成功返回。
如果超时之时,数据仍然未能成功发送及被确认,用上述a方式来关闭此连接。close()返回EWOULDBLOCK。
SO_LINGER选项使用如下结构:
struct linger { int l_onoff; int l_linger; };
l_onoff为0,则该选项关闭,l_linger的值被忽略,close()用上述缺省方式关闭连接。
l_onoff非0,l_linger为0,close()用上述a方式关闭连接。
l_onoff非0,l_linger非0,close()用上述b方式关闭连接。
#define TRUE 1 #define FALSE 0 int z; int s; struct linger so_linger; so_linger.l_onoff = TRUE; so_linger.l_linger = 0; z = setsockopt(s, SOL_SOCKET, SO_LINGER, &so_linger, sizeof so_linger); if ( z ) perror("setsockopt(2)"); close(s); /* Abort connection */
17.accept
accept() 系统调用取出在监听套接口请求队列里的第一个连接,新建一个已连接的套接口,并且返回一个引用该套接口新的文件描述符。新建的套接口不处于监听状态。原始的套接口 sockfd 没有受到影响。
如果全连接队列为空,并且套接口标记为阻塞,accept()会阻塞当前调用进程直到有一个连接出现。
如果全连接队列为空,同时套接口被标记为非阻塞,accept() 返回EAGAIN 或 EWOULDBLOCK 错误。
18. libevent使用
- 创建一个event_base;
- 创建一个event,指定待监听的fd,待监听事件的类型,以及事件放生时的回调函数及传给回调函数的参数;
- 将event添加到event_base的事件管理器中;
- 开启event_base的事件处理循环;
- (异步)当事件发生的时候,调用前面设置的回调函数。
struct event_base* base = event_base_new(); //创建事件管理器
struct event* ev_listen = event_new(base, listener, EV_READ | EV_PERSIST, accept_cb, base); //创建事件,指定触发类型和回调函数,回调函数需要传入事件管理器
event_add(ev_listen, NULL); //往事件管理器中添加事件
event_base_dispatch(base); //开启循环处理
回调函数:
void accept_cb(int fd, short events, void* arg) { evutil_socket_t sockfd; struct sockaddr_in client; socklen_t len = sizeof(client); sockfd = ::accept(fd, (struct sockaddr*)&client, &len); evutil_make_socket_nonblocking(sockfd); struct event_base* base = (event_base*)arg; struct event* ev = event_new(NULL, -1, 0, NULL, NULL);//在回调函数中创建一个事件,
event_assign(ev, base, sockfd, EV_READ | EV_PERSIST, socket_read_cb, (void*)ev);//指定回调函数 event_add(ev, NULL);//并添加到事件管理器中。 }
https://www.jianshu.com/p/8ea60a8d3abb