Unix/Linux支持伯克利风格的套接字编程,它同时支持面向连接和面向无连接类型的套接字。
套接字最常用的一些系统调用:
socket()
bind()
connect()
listen()
accept()
send()
recv()
sendto()
recvfrom()
close()
shutdown()
setsockopt()
getsockopt()
getpeername()
getsockname()
gethostbyname()
gethostbyaddr()
getservbyname()
getservbyport()
getprotobyname()
fcntl()
下面详解这些系统调用。
1、socket()函数
#include <sys/socket.h>
/*成功返回非负描述符,否则返回-1*/
int socket(int family, int type, int protocol);
其中,family参数指明协议族,常用的family值有AF_INET(IPv4协议)、AF_INET6(IPv6协议)、AF_LOCAL(Unix域协议)、AF_ROUTE(路由套接字)和AF_KEY(密钥套接字)。该参数也往往被称为协议域。注意,后两种仅适用于原始套接字。
type参数指明套接字类型,如SOCK_STREAM(字节流套接字)、SOCK_DGRAM(数据报套接字)、SOCK_SEQPACKET(有序分组套接字)以及SOCK_RAW(原始套接字)等。
protocol参数可以设置为IPPROTO_CP(TCP传输协议)、IPPROTO_UDP(UDP传输协议)或IPPROTO_SCTP(SCTP传输协议),同时该参数也可设置为0,以选择所给定family和type组合的系统默认值。
socket()函数在成功时返回一非负整数,它与文件描述符类似,我们称之为套接字描述符(Socket Descriptor),简称sockfd。
那么,此函数的作用是什么呢?socket函数通过我们设定的协议族、套接字类型和传输协议参数来创建底层网络文件,为进行网络通信做准备。
2、bind()函数
bind()函数把一个本地协议地址赋予一个套接字。对于网际网协议,协议地址是32位的IPv4地址或128位的IPv6地址与16位的TCP或UDP端口号的组合。
#include <sys/socket.h>
/*成功返回0,否则返回-1*/
int bind(int sockfd, const struct sockaddr * myaddr, socklen_t addrlen);
参数myaddr指向特定于协议的地址结构的指针,第三个参数是该地址结构的长度。
如果一个TCP客户或服务器未调用bind()捆绑一个端口,当调用connect或listen时,内核就要为相应的套接字选择一个临时端口。让内核选择临时端口对于TCP客户来说是正常的,但对TCP服务器来说极为罕见,因为服务器是通过它们的众所周知的端口被大家所认识的。
进程可以通过bind()函数把一个特定的IP地址绑定到其套接字上,不过此IP地址必须属于其所在主机的网络接口之一。对于TCP客户,这就为该套接字所发送的数据报文指定了源IP地址。对于TCP服务器,这就限定该套接字只接收那些目的地址为此IP地址的客户连接。
对于TCP,调用bind函数可以指定一个端口号,或指定一个IP地址,也可以两者都指定,还可以都不指定。那么,当我们未指定端口号或IP地址时,系统调用会如何处理呢?
一般而言,不指定端口号,bind()函数默认为0,不指定IP地址,函数默认为通配地址。
进程指定 |
结果 |
|
IP地址 |
端口port |
|
通配地址 |
0 |
内核选择IP地址和端口 |
通配地址 |
非0 |
内核选择IP地址,进程指定端口 |
本地IP地址 |
0 |
进程指定IP地址,内核选择端口 |
本地IP地址 |
非0 |
进程指定IP地址和端口 |
注:通配地址为INADDR_ANY。
当使用socket()函数得到套接字描述符后,依情况需要将socket绑定主机上的端口:
如果为服务器进程,需要在端口进行监听(listen)操作,等待连接请求时,往往需要进行bind操作,而且这个端口应该是众所周知的;
如果为客户端进程,需要向远端服务器发起连接(connect)请求,这时,绑定端口是可选的。
附:端口号
TCP、UDP和SCTP三种传输协议使用16位端口号来区分进程。
端口号被划分为以下三段:
*众所周知的端口(0-1023);
*已登记的端口(registered port,1024-49151);
*动态(dynamic)或私用(private)端口(49152-65535)。
3、connect()函数
TCP客户端用connect()函数来建立与TCP服务器的连接。
#include <sys/socket.h>
/*成功返回0,出错返回-1*/
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
第二个、第三个参数分别是一个指向套接字地址结构的指针和该结构的大小。套接字地址结构必须含有服务器的IP地址和端口号。
客户端在调用connect()函数时,没有必要调用bind()函数。我们并不在乎我们本地用什么端口来进行通信,我们在乎的是客户端需要连接到远端服务器的哪个端口。当我们未调用bind()函数时,内核自动选择一个未被使用的本地端口。
关于connect()具体是如何工作的,会在网络协议的TCP三次握手时详细介绍。
4、listen()函数
#include <sys/socket.h>
/*成功返回0,否则返回-1*/
int listen(int sockfd, int backlog);
listen()函数仅由TCP服务器调用,它主要完成两件事:
*当socket()函数创建一个套接字是,其被设为主动套接字,也就是说,它是一个将调用connect()函数来主动发起连接的客户端套接字。listen()函数把一个未连接的主动套接字转换成一个被动套接字,指示内核应接受指向该套接字的连接请求。
*backlog规定了内核应该为相应的套接字排队的最大连接个数。
本函数通常应该在socket()和bind()函数之后,并在调用accept()函数之前调用。
这里,需要理解backlog参数:
内核为任何一个给定的监听套接字维护两个队列:
(1)未完成连接队列(incomplete connection queue),这些套接字处于SYN_RCVD状态;
(2)已完成连接队列(completed connection queue),每个已完成TCP三路握手过程,这些套接字处于ESTABLISHED状态。
下图描绘了监听套接字的两个队列。
每当在未完成队列创建一项时,来自监听套接字的参数就复制到即将建立的连接中。
当来自客户的SYN到达服务器时,服务器TCP在未完成连接队列中创建一个新项,然后相应以三路握手的服务器的SYN响应,其中捎带对客户SYN的ACK。这一项一直保留在未完成连接队列中,直到三路握手的第三个分节(客户对服务器SYN的ACK)到达或该项超时为止。如果三路握手正常完成,该项就从未完成队列移至已完成连接队列的队尾。当进程调用accept()时,已完成连接队列中的队头项将返回到进程,如果该队列为空,那么进程将被投入睡眠,直到TCP在该队列中放入一项才唤醒它。
如果一个客户的SYN到达时,两个队列是满的,那么TCP就会忽略该分节,但不会发送RST。这样做有一个好处:两队列是满的的情况只是暂时的,如果服务器TCP不发送RST,那么客户端TCP就会重发SYN,这样,可能不久就能在这些队列中找到可用空间。
5、accept()函数
#include <sys/socket.h>
/*成功返回非负描述符,出错返回-1*/
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
参数cliaddr和addrlen用来返回已连接的对端进程(客户端)的协议地址。addrlen是值-结果参数:调用前,我们将有*addrlen所引用的整数值置为由cliaddr所指的套接字地址结构的长度,返回时,该整数值即为由内核存放在该套接字地址结构内的确切字节数。
如果accept成功,其返回值是由内核自动生成的一个全新描述符,代表与所返回客户的TCP连接,我们称之为已连接套接字(connected socket)。
本函数最多返回三个值:一个既可能是新套接字描述符也可能是出错指示的整数,客户进程的协议地址(由cliaddr指针所指)以及该地址的大小(由addrlen指针所指)。如果我们对是哪个主机连接了该服务器(客户协议地址)不感兴趣,那么可以把cliaddr和addrlen均置为空指针。
6、send()和recv()函数
这两个函数时最基本的,通过连接的套接字流进行通信的函数。
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buff, size_t nbytes, int flags);
send()参数含义如下:
sockfd代表与远程程序连接的套接字描述符;
buff指针指向发送信息的字符串;
nbytes指发送信息的长度;
flags指发送标记。
send()函数在调用后返回它真正发送数据的长度。但是,此发送数据可能少于参数指定的长度。如果发生错误,则返回-1,错误代码存储在全局标量errno中。
#include<sys/socket.h>
ssize_t recv(int sockfd, void *buff, size_t nbytes, unsigned int flags);
recv()参数含义如下:
sockfd指读取数据的套接字描述符;
buff指针指向存储数据的内存缓存区域;
nbytes是缓存区的最大尺寸;
flags是发送标记。
recv()返回它所真正接收到的长度,也就是存储到buf中数据的长度。如果返回-1则代表发生了错误(比如网络意外中断,对方关闭了套接字连接等),全局变量errno存储了错误代码。
7、sendto()和recvfrom函数
这两个函数时进行无连接的UDP通信时使用的。使用这两个函数,则数据会在没有建立过任何连接的网络上传输。在这里,由于数据报套接字无法对远程主机建立连接,因此,我们在发送数据前需要知道远端主机的IP地址和端口号。
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buff, size_t nbytes, int flags, const struct sockaddr *to, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buff, size_t nbytes, int flags, const struct sockaddr *from, socklen_t *addrlen);
前三个参数sockfd、buff和nbytes:套接字描述符、指向读入写出缓冲区的指针和读写字节数。
sendto的to参数指向一个含有数据报接收者的协议地址的套接字地址结构,其大小由addrlen参数指定。recvfrom的from参数指向一个将由该函数在返回时填写数据包发送者的协议地址的套接字地址结构。
注意:sendto的最后一个参数是整数值,而recvfrom的最后一个参数是一个指向整数值的指针(即值-结果参数)。
recvfrom的最后两个参数类似于accept的最后两个参数,返回时其中套接字地址结构的内容告诉我们是谁发送了数据报(UDP情况下)或是谁发起了连接(TCP情况下)。sendto的最后两个参数类似于connect的最后两个参数,调用时其中套接字结构被我们填入数据报将发往(UDP情况下)或与之建立连接(TCP情况下)的协议地址。
这两个函数都把所读写的数据的长度作为函数返回值。
注意:recvfrom和sendto都可以用于TCP,尽管通常没有理由这样做。
8、close()和shutdown()函数
Unix使用close函数和shutdown函数来关闭套接字,并终止TCP连接。
#include <unistd.h>
int close(int sockfd);
/*若成功则返回0,出错返回-1*/
close一个TCP套接字的默认行为是把该套接字标记为关闭,然后立即返回到调用进程。该套接字描述符不能再由调用进程使用,也就是说它不能再作为read和write的第一个参数。然而,TCP将尝试发送已排队等待发送到对端的数据,发送完毕后发生的是正常的TCP连接终止序列。
#include <sys/socket.h>
int shutdown(int sockfd, int how);
注意其中的how参数。0表示不允许以后数据的接收操作,1表示不允许以后数据的发送操作,2表示和close()一样,不允许以后的任何数据操作。
附加内容:
send/recv与write/read函数的区别
recv和send函数提供了和read和write差不多的功能,但是它们提供了第四个参数来控制读写操作。
ssize_t send(int sockfd, const void *buff, size_t nbytes, int flags);
ssize_t recv(int sockfd, void *buff, size_t nbytes, unsigned int flags);
前面的三个参数和read,write相同,第四个参数能够是0或是以下的组合:
flags |
说明 |
recv |
send |
MSG_DONTROUTE |
绕过路由表查找 |
|
* |
MSG_DONTWAIT |
仅本操作阻塞 |
* |
* |
MSG_OOB |
发送或接收带外数据 |
* |
* |
MSG_PEEK |
窥看外来消息 |
* |
|
MSG_WAITALL |
等待所有数据 |
* |
|
如果flags为0,则和read,write一样的操作。
PS.在下一系列博客,将会涉及到具体的网络编程例子。