Unix网络编程 之 基本套接字调用(一)
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或UDPport号的组合。
#include <sys/socket.h>
/*成功返回0,否则返回-1*/
int bind(int sockfd, const struct sockaddr * myaddr, socklen_t addrlen);
參数myaddr指向特定于协议的地址结构的指针,第三个參数是该地址结构的长度。
假设一个TCP客户或server未调用bind()捆绑一个port。当调用connect或listen时,内核就要为对应的套接字选择一个暂时port。让内核选择暂时port对于TCP客户来说是正常的,但对TCPserver来说极为罕见,由于server是通过它们的众所周知的port被大家所认识的。
进程能够通过bind()函数把一个特定的IP地址绑定到其套接字上。只是此IP地址必须属于其所在主机的网络接口之中的一个。对于TCP客户。这就为该套接字所发送的数据报文指定了源IP地址。对于TCPserver,这就限定该套接字仅仅接收那些目的地址为此IP地址的客户连接。
对于TCP。调用bind函数能够指定一个port号,或指定一个IP地址。也能够两者都指定。还能够都不指定。
那么。当我们未指定port号或IP地址时。系统调用会怎样处理呢?
一般而言。不指定port号,bind()函数默觉得0。不指定IP地址。函数默觉得通配地址。
进程指定 |
结果 |
|
IP地址 |
端口port |
|
通配地址 |
0 |
内核选择IP地址和port |
通配地址 |
非0 |
内核选择IP地址。进程指定port |
本地IP地址 |
0 |
进程指定IP地址,内核选择port |
本地IP地址 |
非0 |
进程指定IP地址和port |
注:通配地址为INADDR_ANY。
当使用socket()函数得到套接字描写叙述符后,依情况须要将socket绑定主机上的port:
假设为server进程。须要在port进行监听(listen)操作。等待连接请求时。往往须要进行bind操作,并且这个port应该是众所周知的;
假设为client进程,须要向远端server发起连接(connect)请求。这时,绑定port是可选的。
附:port号
TCP、UDP和SCTP三种传输协议使用16位port号来区分进程。
port号被划分为下面三段:
*众所周知的port(0-1023);
*已登记的端口(registered port,1024-49151)。
*动态(dynamic)或私用(private)port(49152-65535)。
3、connect()函数
TCPclient用connect()函数来建立与TCPserver的连接。
#include <sys/socket.h>
/*成功返回0,出错返回-1*/
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
第二个、第三个參数各自是一个指向套接字地址结构的指针和该结构的大小。套接字地址结构必须含有server的IP地址和port号。
client在调用connect()函数时,没有必要调用bind()函数。
我们并不在乎我们本地用什么port来进行通信,我们在乎的是client须要连接到远端server的哪个port。当我们未调用bind()函数时,内核自己主动选择一个未被使用的本地port。
关于connect()具体是怎样工作的,会在网络协议的TCP三次握手时具体介绍。
4、listen()函数
#include <sys/socket.h>
/*成功返回0,否则返回-1*/
int listen(int sockfd, int backlog);
listen()函数仅由TCPserver调用,它主要完毕两件事:
*当socket()函数创建一个套接字是,其被设为主动套接字,也就是说,它是一个将调用connect()函数来主动发起连接的client套接字。listen()函数把一个未连接的主动套接字转换成一个被动套接字。指示内核应接受指向该套接字的连接请求。
*backlog规定了内核应该为对应的套接字排队的最大连接个数。
本函数通常应该在socket()和bind()函数之后。并在调用accept()函数之前调用。
这里。须要理解backlog參数:
内核为不论什么一个给定的监听套接字维护两个队列:
(1)未完毕连接队列(incomplete connection queue)。这些套接字处于SYN_RCVD状态;
(2)已完毕连接队列(completed connection queue),每一个已完毕TCP三路握手过程,这些套接字处于ESTABLISHED状态。
下图描绘了监听套接字的两个队列。
每当在未完毕队列创建一项时,来自监听套接字的參数就拷贝到即将建立的连接中。
当来自客户的SYN到达server时。serverTCP在未完毕连接队列中创建一个新项,然后对应以三路握手的server的SYN响应,当中捎带对客户SYN的ACK。这一项一直保留在未完毕连接队列中,直到三路握手的第三个分节(客户对serverSYN的ACK)到达或该项超时为止。
假设三路握手正常完毕。该项就从未完毕队列移至已完毕连接队列的队尾。当进程调用accept()时。已完毕连接队列中的队头项将返回到进程。假设该队列为空,那么进程将被投入睡眠。直到TCP在该队列中放入一项才唤醒它。
假设一个客户的SYN到达时,两个队列是满的。那么TCP就会忽略该分节,但不会发送RST。这样做有一个优点:两队列是满的的情况仅仅是临时的。假设serverTCP不发送RST。那么clientTCP就会重发SYN,这样,可能不久就能在这些队列中找到可用空间。
5、accept()函数
#include <sys/socket.h>
/*成功返回非负描写叙述符,出错返回-1*/
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
參数cliaddr和addrlen用来返回已连接的对端进程(client)的协议地址。
addrlen是值-结果參数:调用前,我们将有*addrlen所引用的整数值置为由cliaddr所指的套接字地址结构的长度,返回时,该整数值即为由内核存放在该套接字地址结构内的确切字节数。
假设accept成功。其返回值是由内核自己主动生成的一个全新描写叙述符,代表与所返回客户的TCP连接,我们称之为已连接套接字(connected socket)。
本函数最多返回三个值:一个既可能是新套接字描写叙述符也可能是出错指示的整数,客户进程的协议地址(由cliaddr指针所指)以及该地址的大小(由addrlen指针所指)。假设我们对是哪个主机连接了该server(客户协议地址)不感兴趣,那么能够把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地址和port号。
#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.在下一系列博客,将会涉及到详细的网络编程样例。