赞
踩
socket的主要API都定义在sys/socket.h头文件中,包括创建socket、命名socket、监听socket、接受连接、发起连接、读写数据、获取地址信息、检测带外标记以及读取和设置socket选项。
UNIX/Linux的一个哲学是:所有东西都是文件。socket也不例外,它就是可读、可写、可控制、可关闭的文件描述符。下面的socket系统调用可创建一个socket:
#include
#include
int socket(int domain, int type, int protocol);
domain参数告诉系统使用哪个底层协议。对TCP/IP协议族而言,该参数应该设置为:
type参数指定服务类型:
这里需要注意的是,自Linux2.6.17起,type参数可以接受上述服务类型与下面两个重要的标志的相或值:SOCK_NONBLOCK和SOCK_CLOEXEC。它们分别表示将新创建的socket设为非阻塞的,以及用fork调用创建子进程时在子进程中关闭该socket。在内核版本2.6.17之前的Linux中,文件描述符的这两个属性都需要使用额外的系统调用(比如fcntl)来设置。
protocol参数是在前两个参数构成的协议集合下,再选择一个具体的协议。不过这个值通常都是唯一的(前两个值已经完全决定了它的值)。几乎在所有情况下,我们都应该把它设置为0,表示使用默认协议。
socket系统调用成功时返回一个socket文件描述符,失败则返回-1并设置errno。
创建socket时,我们给它指定了地址族,但是并未指定使用该地址族的哪个具体socket地址。
将一个socket与socket地址绑定称为给socket命名。
命名socket的系统调用是bind,其定义如下:
#include
#include
int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);
bind将my_addr所指的socket地址分配给未命名的sockfd文件描述符,addrlen参数指出该socket地址的长度。
bind成功时返回0,失败则返回-1并设置errno。其中两种常见的errno是EACCESS和EADDRINUSE,含义分别是:
socket被命名之后,还不能马上接受客户连接,我们需要使用如下系统调用来创建一个监听队列以存放待处理的客户连接:
#include
int listen(int sockfd, int backlog);
sockfd参数指定被监听的socket。backlog参数指示内核监听队列的最大长度。监听队列的长度如果超过backlog,服务器将不受理新的客户连接,客户端也将收到ECONNREFUSED错误信息。
在内核2.2之前的Linux中,backlog参数是指所有处于半链接状态(SYN_RCVD)和完全连接状态(ESTABLISHED)的socket的上限。但自内核版本2.2之后,它只表示处于完全连接状态的socket的上限。
处于半连接状态的socket的上限则由下图参数表示。
backlog参数的典型值是5。
listen成功返回0,失败则返回-1并设置errno。
下面我们编写一个服务器程序以研究backlog参数对listen系统调用的影响。
#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 const *argv[]){ signal(SIGTERM, handle_term); if(argc <= 3){ printf("usage: %s ip_address port_number backlog", basename(argv[0])); return 1; } const char* ip = argv[1]; int port = atoi(argv[2]); int backlog = atoi(argv[3]); int sock = socket(PF_INET, SOCK_STREAM, 0); assert(sock >= 0); 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 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;}
可见,在监听队列中,处于ESTABLISHED状态的连接只有6个(backlog值加1)。
下面的系统调用从listen监听队列中接受一个连接:
#include
#include
int accept( int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd参数是执行过listen系统调用的监听socket。addr参数用来获取被连接的远端socket地址,该socket地址的长度由adrlen参数指出。accept成功时返回一个新的连接socket,该socket唯一地标识了被接受这个连接,服务器可通过读写该socket来与被接受对应的客户端通信。
accept失败时返回-1并设置errno。
现在考虑如下情况:如果监听队列中处于ESTABLISHED状态的连接对应的客户端出现网络异常(比如掉线),或者提前退出,那么服务器对这个连接执行的accept调用是否成功。
#include #include #include #include #include #include #include #include #include #include int main(int argc, char const *argv[]){ if(argc <= 2){ printf("usage: %s ip_address port_number", 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); ret = listen(sock, 5); assert(ret != -1); sleep(20); 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 - %s", errno, strerror(errno)); } else{ char remote[INET_ADDRSTRLEN]; printf("connected with ip: %s and port: %d", inet_ntop(AF_INET, &client.sin_addr, remote, INET_ADDRSTRLEN), ntohs(client.sin_port)); close(connfd); } close(sock); return 0;}
我们要在20s,完成如下操作,我们在10.0.0.200上运行该服务:
并在10.0.0.199上运行并立即断开网络:
然后发现accept调用能够正常返回,输入如下:
connected with ip: 10.0.0.199 and port: 41376
通过netstat抓取到:
下面我们重新执行上述过程,不过这次我们不断开客户端网络连接,而是在建立连接后立即推出客户端程序:
这次accept调用同样正常返回,服务器输出如下:
connected with ip: 10.0.0.199 and port: 41378
用netstate抓取到:
由此可见,accept只是从监听队列中取出连接,而不论连接处于何种状态(如上面的ESTABLISHED状态和CLOSE_WAIT状态),更不关心任何网络状况的变化。
如果说服务器通过listen调用来被动发起连接,那么客户端需要通过如下系统调用来主动与服务器建立连接:
#include
#include
int connect( int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
sockfd参数是由socket系统调用返回一个socket。serv_addr参数是服务器监听的socket地址,addrlen参数则指定这个地址的长度。
connect成功时返回0。一旦成功建立连接,sockfd就唯一地标识了这个连接,客户端就可以通过读写sockfd来与服务器通信。connect失败返回-1。并设置errno。
其中两种常见的errno是ECONNREFUSED和ETIMEDOUT,含义如下:
关闭一个连接实际上就是关闭该连接对应的sockfd,这可以通过如下关闭普通文件描述符的系统调用来完成:
#include
int close ( int fd);
fd参数是待关闭的sockfd。不过,close系统调用并非总是立即关闭一个连接,而是将fd的引用计数减1。只有当fd的引用技术为0时,才真正关闭连接。多进程程序中,一个fork系统调用默认将使父进程中打开的socket引用技术加1,因此,我们必须在父进程和子进程中都对该socket执行close调用才能将连接关闭。
如果无论如何都要立即终止连接(而不是将socket的引用计数减1),可以使用如下的shutdown系统调用(相对于close来说,它是专门为网络编程设计的):
#include
int shutdown( int sockfd, int howto);
sockfd参数是待关闭的socket。howto参数决定了shutdown的行为,可选值如下:
由此可见,shutdown能够分别关闭socket上的读或写,或者都关闭。而close在关闭连接时只能将socket上的读和写同时关闭。
shuwdown成功时返回0,失败返回-1并设置errno。
对文件的读写操作read和write同样适用于socket。但是socket编程接口提供了几个专门用于socket数据读写的系统调用,它们增加了对数据读写的控制。其中用于TCP流数据读写的系统调用是:
#include
#include
size_t recv( int sockfd, void* buf, size_t len, int flags);
size_t send( int sockfd, const void* buf, size_t len, int flags);
recv读取sockfd上的数据,buf和len参数分别指定读缓冲区的位置和大小,flags参数,通常设置为0即可。recv成功时返回实际读取到的数据的长度,它可能小于哦我们期望的长度len。因此我们可能要多次调用recv,才能读取到完成的数据。recv可能返回0,这意味着通信对方已经关闭连接了。recv出错时返回-1并设置errno。
send往sockfd上写入数据,buf和len参数分别指定写缓冲区的位置和大小。send成功时返回实际写入的数据的长度,失败则返回-1并设置errno。
flags参数为数据收发提供了额外的控制,如下表:
下面举例说明如何使用这些选项,这里应用MSG_OOB选项给应用程序提供了发送和接收带外数据的方法。
发送带外数据的代码:
#include #include #include #include #include #include #include #include int main(int argc, char const *argv[]){ if(argc <= 2){ printf("usage: %s ip_address port_number", basename(argv[0])); return 1; } const char* ip = argv[1]; int port = atoi(argv[2]); struct sockaddr_in server_address; bzero(&server_address, sizeof(server_address)); server_address.sin_family = AF_INET; inet_pton(AF_INET, ip, &server_address.sin_addr); server_address.sin_port = htons(port); int sockfd = socket(PF_INET, SOCK_STREAM, 0); assert(sockfd >= 0); if(connect(sockfd, (struct sockaddr*)&server_address, sizeof(server_address)) < 0){ printf("connection failed"); } else{ const char* oob_data = "abc"; const char* normal_data = "123"; send(sockfd, normal_data, strlen(normal_data), 0); send(sockfd, oob_data, strlen(oob_data), MSG_OOB); send(sockfd, normal_data, strlen(normal_data), 0); } return 0;}
接受带外数据的代码:
#include #include #include #include #include #include #include #include #include #define BUF_SIZE 1024int main(int argc, char const *argv[]){ if (argc <= 2){ printf("usage: %s ip_address port_number", 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); 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", errno); } else{ char buffer[BUF_SIZE]; memset(buffer, '0', BUF_SIZE); ret = recv(connfd, buffer, BUF_SIZE-1, 0); printf("got %d bytes of normal data '%s", ret, buffer); memset(buffer, '0', BUF_SIZE); ret = recv(connfd, buffer, BUF_SIZE-1, MSG_OOB); printf("got %d bytes of oob data '%s", ret, buffer); memset(buffer, '0', BUF_SIZE); ret = recv(connfd, buffer, BUF_SIZE-1, 0); printf("got %d bytes of normal data '%s", ret, buffer); } return 0;}
可以看到客户端发送给服务端的3个字节的带外数据“abc"中,仅有最后一个字符”c"被服务器当成真正的带外数据接受。并且,服务器对正常数据的接受将被带外数据截断,即前一部分正常数据”123ab"和后续的正常数据“123”是不能被一个recv调用全部读出的。
这里我们观察第五条tcpdump抓取到的数据,输出标志是U,这表示该TCP报文段的头部被设置了紧急标志,“ urg 3"是紧急偏移值,它指出带外数据在字节流中的位置的下一个字节是7(3+4,其中4是该TCP报文段的序号值相对初始序号值的偏移)。因此带外数据是字节流中的第6个字节,即字符”c"。
这里需要注意的是,flags参数只对send和recv的当前调用生效。
socket编程接口中用于UDP数据报读写的系统调用是:
#include
#include
ssize_t recvfrom(int sockfd, void* buf, size_t len, int flags, struct sockaddr* src_addr, socklen_t *addr);
size_t sendto(int sockfd, const void* buf, size_t len, int flags, const struct sockaddr* dest_addr, socklen_t addrlen);
recvfrom读取sockfd上的数据,buf和len参数分别指定读缓冲区的位置和大小。因为UDP通信没有连接的概念,所以我们每次读取数据都要获取发送端的socket地址,及参数src_addr所指的内容,addrlen参数则指定该地址的长度。
sendto往sockfd上写入数据,buf和len参数分别指定写缓冲区的位置和大小。dest_addr参数指定接收端的socket地址,addrlen参数则指定该地址的长度。
这两个系统调用的flags参数以及返回值的含义与send/recv系统调用的flags参数及返回值相同。
值得一提的是,recvfrom/sendto系统调用也可用于面向连接(STREAM)的socket的数据读写,只需要把最后两个参数都设置成NULL以忽略发送端/接收端的socker地址。
socket编程接口还提供了一对通用的数据读写系统调用,它们不仅能用于TCP数据流,也能用于UDP数据报:
#include
sszie_t recvmsg(int sockfd, struct msghdr *msg, int flags);
sszie_t sendmsg(int sockfd, struct msghdr *msg, int flags);
sockfd参数指定被操作的目标socket。msg参数是msghdr结构体类型的指针,msghdr结构体的定义如下:
struct msghdr{ void* msg_name; /*sock地址*/ socklen_t msg_namelen; /*socket地址的长度*/ struct iovec* msg_iov; /*分散的内存块*/ int msg_iovlen; /*分散内存块的数量*/ void *msg_control; /*指向辅助数据的起始位置*/ socklen_t msg_controllen; /*辅助数据的大小*/ int msg_flags; /*复制函数中的flags参数,并在调用过程中更新*/};
msg_name成员指向一个sock地址结构变量。它制定通信对方的socket地址,对于面向连接的TCP含义,该成员没有任何意义,必须设置成NULL。这是因为对数据流socket而言,对方的地址已经知道。
msg_namelen成员则指定了msg_name所指的socket地址的长度。
msg_iov成员是iovec结构体类型的指针,iovec结构体的定义如下:
struct iovec{ void *iov_base; /*内存起始地址*/ size_t iov_len; /*这块内存的长度*/}
因此,iovec结构体封装了一块内存的起始位置和长度。msg_iovlen指定这样的iovec结构体有多少个。对于recvmsg而言,数据将被读取并存放在msg_iovlen快分散的内存中,这些内存的位置和长度由msg_iov指向的数组指定,这称为分散读(scatter read);对于sendmsg而言,msg_iovlen块分散内存中的数据将被一并发送,这称为集中写(gather write)。
msg_control和msg_controllen成员用于辅助数据的发送。
msg_flags成员无需设定,它会复制recvmsg/sendmsg的flags参数的内容以影响数据读写过程。recvmsg还没在调用结束前,将某些更新后的标志设置到msg_flags中。
recvmsg/sendmsg的flags参数以及返回值的含义与send/recv的flags参数以及返回值相同。
之前的代码中演示了TCP带外数据的结构方法。但在实际应用中。我们通常无法预期带外数据何时到来。好在Linux内核检测到TCP紧急标志时,将通知应用程序有带外数据接受。内核通知应用程序带外数据到达的两种常用方式是:IO复用产生的异常事件和SIGURG信号。但是,即使应用程序得到了有带外数据需要接收的通知,还需要知道带外数据在数据流中的具体位置,才能准确接收到带外数据。这一点可以通过如下系统调用实现:
#include
int sockatmark(int sockfd);
sockatmark判断sockfd是否处于带外标记,即下一个被读取到的数据是否是带外数据。如果是,sockarmark返回1,此时我们就可以利用带MSG_OOB标志的recv调用来接受带外数据。如果不是,则sockatmark返回0。
在某些情况下,我们想知道一个连接socket的本端socket地址,以及远端的socket地址。下面这两个函数正是用于解决这个问题:
#include
int getsockname (int sockfd, struct sockaddr* address, socklen_t * address_len);
int getpeername (int sockfd, struct sockaddr* address, socklen_t * address_len);
getsockname获取sockfd对应的本端socket地址,并将其存储于address参数指定的内存中,该socket地址的长度则存储于address_len参数指向的变量中。如果实际socket地址的长度大于address所指内存的大小,那么该socket地址被截断。getsockname成功时返回0,失败返回 -1并设置errno。
getpeername获取sockfd对应的远端socket地址,其参数及返回值的含义与getsockname的参数及返回值相同。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。