赞
踩
socket是一种IPC方法,它允许位于同一主机或者使用网络连接起来的不同主机上的应用程序之间交换数据。
关键的 socket 系统调用包括以下几种。
socket I/O 可以使用传统的 read()和 write()系统调用或使用一组 socket 特有的系统调用(如send()、recv()、sendto()以及 recvfrom())来完成。在默认情况下,这些系统调用在 I/O 操作无法被立即完成时会阻塞。通过使用 fcntl() F_SETFL 操作来启用 O_NONBLOCK 打开文件状态标记可以执行非阻塞 I/O。
在linux上可以通过调用ioctl(fd, FIONREAD, &cnt)来获取文件描述符fd引用的流socket中可用的未读字节数。对于数据报socket来讲,这个操作会返回下一个未读数据报中的字节数(如果下一个数据报的长度为零的话就返回零)或在没有未决数据报的情况下返回 0。这种特性没有在 SUSv3 中予以规定。
NAME listen - listen for connections on a socket SYNOPSIS #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int listen(int sockfd, int backlog); DESCRIPTION listen()系统调用将文件描述符 sockfd 引用的流 socket 标记为被动。这个 socket 后面会被 用来接受来自其他(主动的)socket 的连接 backlog规定了内核应该为这个套接字排队的最大连接个数 无法在一个已连接的 socket(即已经成功执行 connect()的 socket 或由 accept()调用返回的 socket)上执行 listen()。 RETURN VALUE 成功0, 错误-1并设置error
listen仅由TCP服务器调用,它做两件事:
listen
函数把一个未连接的套接字转换成一个被动套接字,指示内核应接收指向该套接字的连接请求
listen
之后,套接字从CLOSED
状态转换为LISTEN
状态backlog
规定了内核应该为这个套接字排队的最大连接个数,也就是说未完成连接队列的大小ECONNREFUSED
错误。要理解 backlog 参数的用途首先需要注意到客户端可能会在服务器调用 accept()之前调用connect()。这种情况是有可能会发生的,如服务器可能正忙于处理其他客户端。这将会产生一个未决的连接,如下图所示:
内核必须要记录所有未决的链接请求的相关信息,这样后继的accept()就能够处理这些请求了。backlog参数允许限制这种未决连接的数量。
在内核版本2.2之前的linux中,backlog参数是指所有处于半连接状态(SYNC_RCVD)和完全连接状态(ESTABLISHED)的socket的上限。
在内核版本2.2之后,它只表示处于完全连接状态的socket的上限,处于半连接状态的socket的上限则由/proc/sys/net/ipv4/tcp_max_syn_backlog
内核参数定义。backlog参数的典型值是5
内核会为任何一个给定的监听套接字维护两个队列:
SYN
之后,而服务器还没有和这个连接进行TCP三次握手的时候,这些套接字处理SYN_SEND
状态。ESTABLISHED
状态accept
之前到达的数据应该由服务器TCP排队,最大数据量为相应已连接套接字的接收缓冲区大小void Listen(int fd, int backlog)
{
char *ptr;
if ( (ptr = getenv("LISTENQ")) != NULL)
backlog = atoi(ptr);
if (listen(fd, backlog) < 0){
printf("listen error");
exit(0);
}
}
问:backlog参数对listen系统调用的实际影响
(1) 先编写服务器程序
#include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <signal.h> #include <unistd.h> #include <stdlib.h> #include <assert.h> #include <stdio.h> #include <string.h> #include <stdbool.h> #include <memory.h> static bool stop = false; static void handle_term( int sig ) { stop = true; } int main( int argc, char* argv[] ) { struct sockaddr_in address; const char* ip; int port, backlog, sock; int ret; signal( SIGTERM, handle_term ); if( argc <= 3 ) { printf( "usage: %s ip_address port_number backlog\n", basename( argv[0] ) ); return 1; } ip = argv[1]; port = atoi( argv[2] ); backlog = atoi( argv[3] ); sock = socket( PF_INET, SOCK_STREAM, 0 ); assert( sock >= 0 ); bzero( &address, sizeof( address ) ); address.sin_family = AF_INET; address.sin_port = htons( port ); inet_pton( AF_INET, ip, &address.sin_addr ); ret = bind( sock, ( struct sockaddr* )&address, sizeof( address ) ); assert( ret != -1 ); ret = listen( sock, backlog ); assert( ret != -1 ); while ( ! stop ) { sleep( 1 ); } close( sock ); return 0; }
(2)命令行执行
并且使用telnet 192.168.0.12 12345
多次建立连接,每执行一次就使用netstat -nt | grep 12345
来查看状态
accept()系统调用在文件描述符 sockfd 引用的监听流 socket 上接受一个接入连接。如果在调用 accept()时不存在未决的连接,那么调用就会阻塞直到有连接请求到达为止
/*
* 参数:
* * sockfd: 监听套接字,由socket创建
* * cliaddr: 用来返回已连接的客户端的协议地址。如果对客户地址不管兴趣,可以置为NULL
* * addrlen:是值-结果参数,调用前,为cliaddr的地址长度,调用后,为内核存放在该地址结果内的确切字节数。如果对客户地址不管兴趣,可以置为NULL
* 返回值: 出错-1,成功返回已连接套接字
*/
int accept (int sockfd, struct sockaddr *__restrict cliaddr,
socklen_t *__restrict addr_len)
cliaddr
是通过指针方式获取的客户端的地址,addr_len
告诉我们地址的大小。这可以理解为当我们拿起电话机时,看到了来电显示,知道了对方的毫秒问题:为什么要把两个套接字分开呢?用一个不是挺好的么?
问: 如果监听队列中处于
ESTABLISTEN
状态的连接对应的客户端出现网络异常(比如掉线)或者提前退出,那么服务器对这个连接执行的accept调用是否能够成功?
(1)编写代码
#include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <assert.h> #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <errno.h> #include <string.h> int main( int argc, char* argv[] ) { if( argc <= 2 ) { printf( "usage: %s ip_address port_number\n", basename( argv[0] ) ); return 1; } const char* ip = argv[1]; int port = atoi( argv[2] ); struct sockaddr_in address; bzero( &address, sizeof( address ) ); address.sin_family = AF_INET; inet_pton( AF_INET, ip, &address.sin_addr ); address.sin_port = htons( port ); int sock = socket( PF_INET, SOCK_STREAM, 0 ); assert( sock >= 0 ); int ret = bind( sock, ( struct sockaddr* )&address, sizeof( address ) ); assert( ret != -1 ); sleep(20); //暂停20s以便客户端连接、掉线或者退出完成 ret = listen( sock, 5 ); assert( ret != -1 ); struct sockaddr_in client; socklen_t client_addrlength = sizeof( client ); int connfd = accept( sock, ( struct sockaddr* )&client, &client_addrlength ); if ( connfd < 0 ) { printf( "errno is: %d\n", errno ); } else { char remote[INET_ADDRSTRLEN ]; printf( "connected with ip: %s and port: %d\n", inet_ntop( AF_INET, &client.sin_addr, remote, INET_ADDRSTRLEN ), ntohs( client.sin_port ) ); close( connfd ); } close( sock ); return 0; }
(2)执行下面操作
总结: accept调用对客户端网络的断开毫不知情
(3) 执行下面操作
总结:accept只是从监听队列中取出连接,而不论连接处于何种状态,更不关心任何网络的变化
econnrefused
socket_stream* server_socket::accept(int timeout /* = 0 */, bool* etimed /* = NULL */){ if (etimed) { *etimed = false; } if (listen_fd_ == nullptr) { logger_error("server socket not opened!"); return NULL; } if (timeout > 0) { if (listen_fd_->acl_read_wait(timeout) == -1) { if (etimed) { *etimed = true; } return NULL; } } acl_socket* conn_fd = listen_fd_->acl_accept(NULL, 0, NULL); if (conn_fd == NULL) { if (open_flag_ & ACL_NON_BLOCKING) { logger_error("accept error %s", strerror(errno)); } else if (errno != EAGAIN && errno != EWOULDBLOCK) { logger_error("accept error %s", strerror(errno)); return NULL; } } socket_stream *client = new socket_stream(); return client; }
static int accept_ok_errors[] = {
EAGAIN,
ECONNREFUSED,
ECONNRESET,
EHOSTDOWN,
EHOSTUNREACH,
EINTR,
ENETDOWN,
ENETUNREACH,
ENOTCONN,
EWOULDBLOCK,
ENOBUFS, /* HPUX11 */
ECONNABORTED,
0,
};
EINPROGRESS
操作正在进行中。一个阻塞的操作正在执行。
ECONNREFUSED
ENOTCONN
套接字操作需要一个目的地址,但是套接字尚未连接.
我目前正在维护一些网络服务器软件,我需要执行大量的I/O操作。在套接字上使用时,read(),write(),close()和shutdown()调用有时可能会引发ENOTCONN错误。这个错误究竟意味着什么?什么是会触发它的条件?
EISCONN
https://blog.csdn.net/svap1/article/details/70809798
EAGAIN
:
如果连接数目达此上限则client 端将收到ECONNREFUSED 的错
EWOULDBLOCK
用于非阻塞模式,不需要重新读或者写
因为非阻塞,所以,read,write,等等只要不能马上操作就会返回这个错误,它的意思是让软件再尝试直到OK为止。所以,遇见这个错误只要重新再读就可以了
ENOBUFS
可用的缓冲区空间不足。只有释放了足够的资源,才能创建套接字。
ECONNRESET_
连接被远程主机关闭。有以下几种原因:远程主机停止服务,重新启动;当在执行某些操作时遇到失败,因为设置了“keep alive”选项,连接被关闭,一般与ENETRESET一起出现。
1、在客户端服务器程序中,客户端异常退出,并没有回收关闭相关的资源,服务器端会先收到ECONNRESET错误,然后收到EPIPE错误。
2、连接被远程主机关闭。有以下几种原因:远程主机停止服务,重新启动;当在执行某些操作时遇到失败,因为设置了“keep alive”选项,连接被关闭,一般与ENETRESET一起出现。
3、远程端执行了一个“hard”或者“abortive”的关闭。应用程序应该关闭socket,因为它不再可用。当执行在一个UDP socket上时,这个错误表明前一个send操作返回一个ICMP“port unreachable”信息。
4、如果client关闭连接,server端的select并不出错(不返回-1,使用select对唯一一个socket进行non- blocking检测),但是写该socket就会出错,用的是send.错误号:ECONNRESET.读(recv)socket并没有返回错误。
5、该错误被描述为“connection reset by peer”,即“对方复位连接”,这种情况一般发生在服务进程较客户进程提前终止。
ECONNABORTED Software caused connection abort
1、软件导致的连接取消。一个已经建立的连接被host方的软件取消,原因可能是数据传输超时或者是协议错误。
2、该错误被描述为“software caused connection abort”,即“软件引起的连接中止”。原因在于当服务和客户进程在完成用于 TCP 连接的“三次握手”后,客户 TCP 却发送了一个 RST (复位)分节,在服务进程看来,就在该连接已由 TCP 排队,等着服务进程调用 accept 的时候 RST 却到达了。POSIX 规定此时的 errno 值必须 ECONNABORTED。源自 Berkeley 的实现完全在内核中处理中止的连接,服务进程将永远不知道该中止的发生。服务器进程一般可以忽略该错误,直接再次调用accept。
当TCP协议接收到RST数据段,表示连接出现了某种错误,函数read将以错误返回,错误类型为ECONNERESET。并且以后所有在这个套接字上的读操作均返回错误。错误返回时返回值小于0。
https://blog.csdn.net/wuji0447/article/details/78356875
服务器是通过listen
调用被动接受连接,而客户端通过connect主动发起连接:
/*
* 功能: TCP客户使用connect函数来建立与TCP服务器的连接
* 返回值: 成功0,出错-1
*/
int connect (int soccket, const struct sockaddr * servaddr, socklen_t len)
bind
指定IP地址和端口,内核就会自己确定源IP地址,并选择一个临时端口作为源端口connect
函数将激发TCP的三次握手工程,并且仅在连接建立成功或者出错才返回,其中出错原因可能如下:(1)ETIMEDOUT
SYN
包没有任何响应,返回TIMEDOUT
错误。如果第一次过6s没有收到响应,过再发一次SYN,24s之后没有响应再发一次。。。75s之后仍没有反应就返回ETIMEDOUT
当然我们可以自己定义超时时间
(2) ECONNREFUSED
RST
(复位)(这是一种硬错误(hard error))回答,这个时候客户端(connect
)一收到RST就马上返回ECONNREFUSED
错误(3)EHOSTUNREACH
、UNETUNREACH
destination unreachable(目的地不可达)
的ICMP错误。
ehostunreach
和enetunreach
错误返回给进程。connect
函数导致当前套接字从CLOSE
状态转移到SYN_SENT
状态,如果成功则再转移到established
状态。如果connect
失败将导致该套接字不可用,必须关闭,我们不能对这样的套接字再次调用connect
(4)ECONNRESET
其他错误: 当我们以非阻塞的方式来进行连接的时候,返回的结果如果是 -1,这并不代表这次连接发生了错误,如果它的返回结果是 EINPROGRESS
,那么就代表连接还在进行中。 后面可以通过poll或者select来判断socket是否可写,如果可以写,说明连接完成了。
void Connect(int fd, const struct sockaddr *sa, socklen_t salen)
{
if (connect(fd, sa, salen) < 0){
printf("connect error");
exit(0);
}
}
close
函数#include <unistd.h>
/*
* 功能: 套接字引用计数减1.当为0时立即关闭连接
* 返回值: 成功0,出错-1
*/
int close (int __fd)
并发服务器中父进程关闭已连接套接字只是导致相应描述符的引用计数值减1。如果引用计数值大于0,这个close
调用并不引发TCP的四分组连接终止序列。一旦发现套接字引用计数到0,就会对套接字进行彻底释放,并且会TCP两个方向的数据流
如果我们确实想要在某个TCP连接上发送一个FIN(表示立即终止连接),可以改用shutdown
函数。
问:套接字引用计数的含义
因为套接字可以被多个进程共享,可以理解为我们给每个套接字都设置了一个积分,如果我们通过fork的方式产生子进程,套接字就会积分+1,如果我们调用一次close函数,套接字就会积分-1。这就是套结种子引用计数的含义。
问:“TCP两个方向的数据流”怎么理解
TCP是双向的,这里说的方向,指的是数据“写入-流出”的方向。
问: close函数具体是如何关闭两个方向的数据流呢?
问:如果父进程对每个由
accept
返回的已连接套接字都不调用close
,那么在并发服务器中将会发生什么?
close
这个套接字,那么这个套接字的引用计数值将一直保持为1,这将妨碍TCP连接终止序列的发生,导致连接一直打开着#include <sys/socket.h>
/*
* 功能:
* 返回值:成功0,失败-1
*/
int shutdown(int socket, int howto);
howto 在 Linux 下有以下取值:
问题:使用 SHUT_RDWR 来调用 shutdown 不是和 close 基本一样吗,都是关闭连接的读和写两个方向。
其实,这两个还是有差别的
综上:
在大多数情况下,我们会优选 shutdown 来完成对连接一个方向的关闭,待对端处理完之后,再完成另外一个方向的关闭。
在客户端发起连接请求之前,服务端必须初始化好。因此,先初始化一个socket,然后执行bind将自己的服务能力绑定到一个众所周知的地址和端口上,紧接着,服务端执行listen操作,将原来的socket转换为服务端的socket,服务端最后阻塞在accept上等待客户端请求的到来
此时,服务端已经准备期就绪。接下来是客户端,客户端必须先初始化socket,再执行connect向服务端的地址和端口发起连接请求,这里的地址和端口必须是客户端预先知晓的。这个过程,就是TCP三次握手。
一旦三次握手完成,客户端和服务端建立连接,就进入了数据传输的过程。
具体来说,客户端进程向操作系统内核发起write字节流写操作,内核协议栈将字节流通过网络设备传输到服务端,服务端从内核得到消息,将字节流从内核读入到进程中,并开始业务逻辑的处理,完成之后,服务端再将得到的结果以同样的方式写给客户端。可以看出,一旦连接建立,数据的传输就不再使单向的,而是双向的,这也是TCP的一个显著特点。
当客户端完成和服务端的交互之后,比如执行一次telnet操作,或者一次http请求,需要和服务端断开连接是,就会执行close函数,操作系统内核此时会通过原先的连接链路向服务端发送一个FIN包,服务端收到之后执行被动关闭,这个时候整个链路处于半关闭状态,此后,服务端也会执行close函数,整个链路才会真正关闭。半关闭的状态下,发起close请求的一方在没有收到对方FIN包之前都认为连接是正常的;而在全关闭的状态下,双方都感知连接已经关闭。
可以看到,以上所有的操作,都是通过socket来完成的。无论是客户端的connect,还是服务端的accept,或者read/write操作等,socket是我们用来建立连接,传输数据的唯一途径
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。