本文讲解编写一个完整的TCP客户/服务器程序所需要的基本套接字函数。
socket函数
为了执行网络I/O,一个进程必须做的第一件事情就是调用socket函数,指定期望的通信协议类型(TCP、UDP、Unix域等)
#include
int socket(int family, int type, int protocol);
返回:若成功则为非负描述符,若出错则为-1
参数family指明协议族,他的取值如下图所示:
参数type指明套接字类型,他的取值如下所示:
参数protocol 指定为某个具体的协议类型常值,或者指定为0,根据给定的family和type组合,系统选择默认的值。
参数family 和 type 的有效组合如下图所示:
socket函数在成功时返回一个小的非负整数值,他与文件描述符类似,我们把他称为套接字描述符。
connect函数
Tcp客户端用connect函数来建立与TCP服务器的连接。
#include
int connect (int sockfd , const struct sockaddr* servaddr, socklen_t addrlen);
返回:若成功则为0, 若出错则为-1
sockfd是由socket函数返回的套接字描述符,第二个、第三个参数分别是一个指向套接字地址结构的指针和结构体大小,套接字地址结构必须含有服务器的IP地址和端口号。如果是TCP套接字,调用connect函数将激发TCP的三路握手过程,而且仅在连接建立成功或者出错时才返回,其中出错返回可能有以下几种情况。
1) 若TCP客户端没有收到SYN分节的响应,则返回ETIMEDOUT错误。举例来说,调用connect函数时,4.4BSD内核发送一个SYN,若无响应则等待6s后再发送一个,若仍无响应则等待24s后再发送一个,若总共等了75s后仍没有收到响应则返回该错误。
2) 若对客户的SYN的响应是RST(表示复位),则表明该服务器主机在我们指定的端口上没有进程在等待与之连接,这是一种硬错误(hard error),客户一收到RST就马上返回ECONNREFUSED错误。
产生RST的三个条件:a) 目的地为某端口的SYN到达,但是该端口上没有正在监听的服务器进程。b) TCP想取消一个已有连接。c) TCP收到一个根本不存在的连接上的分节。
3) 若客户发出的SYN在中间的某个路由器上引发一个destination unreachable 目的地不可达的ICMP错误,则认为是一种软错误(soft error)。客户主机内核保存该消息,并按第一种情况种所述的时间间隔连续发送SYN。若在某个规定的时间后仍未收到响应,则把保存的消息作为EHOSTUNREACH或者ENETUNREACH错误给进程。
按照TCP状态转换图,connect函数导致当前套接字从CLOSED状态(该套接字自从由sockect函数创建以来一直所处的状态)转移到SYN_SENT状态,若成功则转移到ESTABLISHED状态。若connect失败则该套接字不再可用,必须关闭,我们不能对这样的套接字再次调用connect函数。即每次connect失败后,都必须调用close函数关闭。
bind函数
bind函数把一个本地协议地址赋予一个套接字。对于网际协议,协议地址是32位的IPV4地址或者128位的IPV6地址与16位的TCP或者UDP端口号的组合。
#include
int bind( int sockfd, const struct sockaddr* myaddr ,socklen_t addrlen);
返回:若成功则位0,若出错则位-1
第二个参数是一个指向特定与协议的地址结构的指针,第三个参数是该地址结构的长度。对于TCP,调用bind函数可以指定一个端口号,或指定一个IP地址,也可以两者都指定,还可以都不指定。
a) 服务器在启动时捆绑他们的众所周知端口,如果一个TCP客户或者服务器未蹭调用bind捆绑一个端口,当调用connect或者listen时,内核就要为相应的套接字选择一个临时端口。让内核来选择临时端口对于TCP客户来说时正常的,除非应用需要一个预留端口;然而对于TCP服务器来说却极为罕见,因为服务器是通过他们的众所周知端口被大家认知的。
b) 进程可以把一个特定的IP地址捆绑到他的套接字上,不过这个IP地址必须属于其所在主机的网络接口之一。对于TCP客户端,这就为在该套接字上发送的IP数据报指派了源IP地址。对于TCP服务器,这就限定该套接字只接收那些目的地为这个IP地址的客户连接。TCP客户通常不把IP地址捆绑到他的套接字上。当连接套接字时,内核将根据所用外出网络接口来选择源IP地址,而所用外出接口则取决于到达服务器所需的路径。如果TCP服务器没有把IP地址捆绑到他的套接字上,内核就把客户发送的SYN的目的IP地址作为服务器的源IP地址。
如果指定端口号为0,那么内核就在bind被调用时选择一个临时端口。然后,如果指定IP地址为通配地址,那么内核将等到套接字已连接(TCP)或者已在套接字上发出数据报(UDP)时才选择一个本地IP地址。
对于IPV4来说,通配地址由常值INADDR_ANY来指定,其值一般为0。他告知内核去选择IP地址。
struct sockaddr_in servaddr ;
servaddr.sin_addr.s_addr =htonl(INADDR_ANY); /* wildcard */
对于IPV6
struct sockaddr_in6 serv;
serv.sin6_addr = in6addr_any; /* wildcard */
如果让内核来为套接字选择一个临时端口号,必须调用函数getsockname来返回协议地址。bind函数返回的一个常见的错误时EADDRINUSE。
listen函数
#include
int listen(int sockfd , int backlog);
返回:若成功则为0,若出错则为-1。
listen函数仅由TCP服务器调用,他做两件事情。
1) 当socket函数创建一个套接字时,他被假设为一个主动套接字,也就是说,他是一个将调用connect发起连接的客户套接字。listen函数把一个未连接的套接字转换成一个被动套接字,指示内核应接受指向该套接字的连接请求。根据TCP状态图,调用listen函数导致套接字从CLOSED状态转换到LISTEN状态。
2) 本函数的第二个参数规定了内核应该为相应套接字排队的最大连接个数。
为了理解backlog参数,我们必须认识到内核为任何一个给定的监听套接字维护两个队列:
a) 未完成连接队列,每个这样的SYN分节对应其中一项:已由某个客户发出并到达服务器,而服务器正在等待完成相应的TCP三路握手过程。这些套接字处于SYN_RCVD状态
b) 已完成连接队列,每个已完成TCP三路握手过程的客户对应其中一项。这些套接字处于ESTABLISHED状态。