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 中予以规定。
在一个典型的客户端/服务器常见中,应用程序使用socket进行通信的方式如下:
使用socket()系统调用能够创建一个socket,它返回一个用来在后继系统调用中应用该socket的文件描述符
NAME
socket -创建一个新 socket
SYNOPSIS
#include /* See NOTES */
#include
int socket(int domain, int type, int protocol);
DESCRIPTION
domain: 通信域
type:类型
protocal:通信范围
RETURN VALUE
如果正确,返回一一个非负int类型的值,表示该socket实例唯一标识符的文件描述符。这个值可以调用其他系统调用来进行各种操作,比如绑定和监听端口、发送数据等。套接字将处于close状态
如果失败,返回-1,并设置error
(1)socket存在于一个通信domain中,它用于确定确定:
其取值如下
domain | 说明 | 应用程序间的通信 | 地址格式 |
---|---|---|---|
AF_INET | IPv4协议 | 通过 IPv4 网络连接起来的主机 | 128 位 IPv6 地址+16 位端口号 |
AF_INET6 | IPv6协议 | 通过 IPv6 网络连接起来的主机 | 32 位 IPv4 地址+16 位端口号 |
AF_UNIX, AF_LOCAL | Unix域协议 | 同一主机(内核中) | 路径名 |
AF_ROUTE | 路由套接字 | ||
AF_KEY | 密钥套接字 |
关于AF和PF:AF_前缀表示地址族,PF_前缀表示协议族 ,因为历史上曾想让一个协议族(PF)支持多个地址族(AF),用PF来创建套接字,用AF来创建套接字地址结构,然而就只是想想,没有实现。现在AF和PF的值是相等的。
现代操作系统至少应该支持如下domain:
(2)type取值
type | 说明 |
---|---|
SOCK_STREAM | 字节流套接字 |
SOCK_DGRAM | 数据报套接字 |
SOCK_SEQPACKET | 有序分组套接字 |
SOCK_RAM | 原始套接字 |
注意:自Linux内核2.6.17起,type参数可以接受SOCK_STREAM和SOCK_DGRAM类型与SOCK_NONBLOCK、SOCK_CLOEXEC相与。它们分别表示将新创建的socket设为非阻塞的从而无需通过调用 fcntl()来取得同样的结果),以及用fork调用创建子进程时在子进程中关闭该socket。
每个 socket 实现都至少提供了两种 socket:流和数据报,其属性总结如下:
属性 | 流 | 数据报 |
---|---|---|
可靠地递送? | 是 | 否 |
消息边界保留? | 否 | 是 |
面向连接? | 是 | 否 |
流socket(SOCK_STREAM)提供了一个可靠的双向字节流通信信道:
(3)protocol取值
protocol | 说明 |
---|---|
IPPROTO_CP(ipproto) | TCP传输协议 |
IPPROTE_UDP | UDP传输协议 |
IPPROTO_SCTP | SCTP传输协议 |
一般都设置为0,因为前两个参数已经完全决定了它的值
/**
* 取得套接字的类型:是网络套接字还是域套接字
* @param fd {ACL_SOCKET} 网络套接字
* @return {int} -1: 表示出错或输入非法或非套接字; >= 0 表示成功获得套接字
* 类型,返回值有 AF_INET、AF_INET6 或 AF_UNIX(仅限 UNIX 平台)
*/
int acl_getsocktype(ACL_SOCKET fd)
{
ACL_SOCKADDR addr;
struct sockaddr *sa = (struct sockaddr*) &addr;
socklen_t len = sizeof(addr);
if (fd == ACL_SOCKET_INVALID) {
return -1;
}
if (getsockname(fd, sa, &len) == -1) {
return -1;
}
#ifdef ACL_UNIX
if (sa->sa_family == AF_UNIX) {
return AF_UNIX;
}
#endif
#ifdef AF_INET6
if (sa->sa_family == AF_INET || sa->sa_family == AF_INET6) {
#else
if (sa->sa_family == AF_INET) {
#endif
return sa->sa_family;
}
return -1;
}
套接字分为两种:
在 Internet domain 中,数据报 socket 使用了用户数据报协议(UDP),而流 socket 则(通常)使用了传输控制协议(TCP)。一般来讲,在称呼这两种 socket 时不会使用术语“Internet domain数据报 socket”和“Internet domain 流 socket”,而是分别使用术语“UDP socket”和“TCP socket”
流socket通常可以分为主动和被动两种:
在大多数使用流 socket 的应用程序中,服务器会执行被动式打开,而客户端会执行主动式打开。
下面 可以判断 :是监听套接字(listen)还是非监听套接字(connect)
/**
* 检查套接字:是监听套接字还是网络套接字
* @param fd {ACL_SOCKET} 套接字句柄
* @return {int} 返回 -1 表示该句柄非套接字,1 为监听套接字,0 为非监听套接字
*/
int acl_check_socket(ACL_SOCKET fd);
/**
* 判断套接字是否为监听套接字
* @param fd {ACL_SOCKET} 套接字句柄
* @return {int} 返回值 0 表示非监听套接字,非 0 表示为监听套接字
*/
int acl_is_listening_socket(ACL_SOCKET fd);
int acl_check_socket(ACL_SOCKET fd)
{
int val, ret;
socklen_t len = sizeof(val);
ret = getsockopt(fd, SOL_SOCKET, SO_ACCEPTCONN, (void*) &val, &len);
if (ret == -1) {
return -1;
} else if (val) {
return 1;
} else {
return 0;
}
}
int acl_is_listening_socket(ACL_SOCKET fd)
{
return acl_check_socket(fd) == 1;
}
一对连接的流socket在两个端点之间提供了一个双向通信通道。如下:
连接流socket上IO的语义与管道上IO的语义类似:
ioctl(keyFd, FIONREAD, &b)
read(keyFd, &b, sizeof(b))
可以,可以通过ioctl探测套接字中系统缓存区的数据长度
/**
* 探测套接字中系统缓存区的数据长度
* @param fd {ACL_SOCKET} 描述符
* @return {int} 系统缓存区数据长度
*/
int acl_peekfd(ACL_SOCKET fd)
{
int count;
/*
* Anticipate a series of system-dependent code fragments.
*/
#ifdef ACL_UNIX
return (ioctl(fd, FIONREAD, (char *) &count) < 0 ? -1 : count);
#elif defined(ACL_WINDOWS)
return (ioctlsocket(fd, FIONREAD, (unsigned long *) &count) < 0
? -1 : count);
#endif
}
NAME
bind - bind a name to a socket
SYNOPSIS
#include /* See NOTES */
#include
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
DESCRIPTION
bind函数把myaddr所指的soket地址分配给未命名(未绑定)的sockfd文件描述符
addr 参数是一个指针,它指向了一个指定该 socket 绑定到的地址的结构。传入这个参数的结构的类型
取决于 socket domain。addrlen 参数指定了地址结构的大小
返回值:
成功0,失败-1并设置error
一般来讲,会将一个服务器的 socket 绑定到一个众所周知的地址——即一个固定的与服务器进行通信的客户端应用程序提前就知道的地址。
调用bind
函数可以指定一个端口号,或者一个IP地址,也可以两者都指定,或者两者都不指定。
(1)如果一个TCP客户端/服务器没有使用bind绑定一个端口,当调用connect
或者listen
的时候,内核就会为相应的套接字选择一个临时端口:
(2)如果一个TCP客户端/服务器使用bind指定端口0作为固定端口,内核就会在调用bind时选择一个临时端口
(3)我们可以在进程中调用bind为进程指定一个IP地址,不过这个IP地址必须属于其所在主机的网络接口之一。
不过TCP客户端一般不手动绑定,内核对自动根据所用外出网络接口来选择源IP地址,而所用外出接口则取决于到达服务器所需的路径。
如果TCP服务器不指定IP地址(INADDR_ANY),内核就会把客户发送SYN的目的IP地址作为服务器的源IP地址
bind失败的常见错误:
bind(int fd, sockaddr * addr, socklen_t len)
sockaddr * addr
是一个通用地址格式。注意,虽然接收的是通用地址格式,实际上传入的参数可能是IPv4、IPv6 或者本地套接字格式。bind函数会根据len判断addr该怎么解析,len字段表示的就是传入的地址长度,它是一个可变值。
这里其实可以把 bind 函数理解成这样:
bind(int fd, void * addr, socklen_t len)
不过 BSD 设计套接字的时候大约是 1982 年,那个时候的 C 语言还没有void *的支持,为了解决这个问题,BSD 的设计者们创造性地设计了通用地址格式来作为支持 bind 和accept 等这些函数的参数。
对于使用者来说,每次需要将 IPv4、IPv6 或者本地套接字格式转化为通用套接字格式,就像下面的 IPv4 套接字地址格式的例子一样:
struct sockaddr_in name;
bind (sock, (struct sockaddr *) &name, sizeof (name)
设置bind的时候,对地址和端口有多种处理方式。
那么该如何设置通配地址呢?
struct sockaddr_in name;
name.sin_addr.s_addr = htonl (INADDR_ANY); /* IPV4 通配地址 */
/**
* 网络地址绑定函数,适用于 TCP/UDP 套接口
* @param res {const struct addrinfo*} 域名解析得到的地址信息对象
* @param flag {unsigned int} 标志位
* @return {ACL_SOCKET} 返回 ACL_SOCKET_INVALID 表示绑定失败
*
*/
ACL_SOCKET acl_inet_bind(const struct addrinfo *res, unsigned flag)
{
ACL_SOCKET fd;
int on;
fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (fd == ACL_SOCKET_INVALID) {
ERROR("%s(%d): create socket %s",
__FILE__, __LINE__, strerror(errno));
return ACL_SOCKET_INVALID;
}
if (flag & ACL_INET_FLAG_EXCLUSIVE) {
#if defined(SO_EXCLUSIVEADDRUSE) // 独占端口不准许别人使用
on = 1;
if (setsockopt(fd, SOL_SOCKET, SO_EXCLUSIVEADDRUSE,
(const void *) &on, sizeof(on)) < 0) {
WARN("%s(%d): setsockopt(SO_EXCLUSIVEADDRUSE)"
": %s", __FILE__, __LINE__, strerror(errno));
}
#endif
} else {
on = 1;
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR,
(const void *) &on, sizeof(on)) < 0) {
WARN("%s(%d): setsockopt(SO_REUSEADDR): %s",
__FILE__, __LINE__, strerror(errno));
}
}
#if defined(SO_REUSEPORT)
on = 1;
if (flag & ACL_INET_FLAG_REUSEPORT) {
int ret = setsockopt(fd, SOL_SOCKET, SO_REUSEPORT,
(const void *) &on, sizeof(on));
if (ret < 0)
WARN("%s(%d): setsocket(SO_REUSEPORT): %s",
__FILE__, __LINE__, strerror(errno));
}
#else
(void) flag;
#endif
#ifdef ACL_WINDOWS
if (bind(fd, res->ai_addr, (int) res->ai_addrlen) < 0) {
#else
if (bind(fd, res->ai_addr, res->ai_addrlen) < 0) {
#endif
close(fd);
ERROR("%s(%d): bind error %s",
__FILE__, __LINE__, strerror(errno));
return ACL_SOCKET_INVALID;
}
return fd;
}
#include
struct sockaddr_in servaddr_v4;
servaddr_v4.sin_addr.s_addr = htonl(INADDR_ANY);
struct sockaddr_in6 servaddr_v6;
servaddr_v6.sin6_addr = in6addr_any;
void Bind(int fd, const struct sockaddr *sa, socklen_t salen)
{
if (bind(fd, sa, salen) < 0){
printf("bind error");
exit(0);
}
}
NAME
listen - listen for connections on a socket
SYNOPSIS
#include /* See NOTES */
#include
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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
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
#include
#include
#include
#include
#include
#include
#include
#include
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
/*
* 功能: 套接字引用计数减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
/*
* 功能:
* 返回值:成功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是我们用来建立连接,传输数据的唯一途径
参考