1.socket函数
为了执行网络I/O,一个进程必须做的第一件事就是调用socket函数,指定期望的通信协议类型
#include <sys/socket.h> int socket (int family, int type, int protocol); //返回:若成功则为非负描述符,若出错则为-1
其中family指明协议族,type参数指明套接字类型,protocol参数应该设为某个(见下图)协议类型常值,或者设为0,以选择所给定family和type组合的系统默认值
socket函数的family常值
family |
说 明 |
AF_INET AF_INET AF_LOCAL AF_ROUTE AF_KEY |
IPv4协议 IPv6协议 Unix域协议 路由套接口 密钥套接口 |
socket函数的type常值
type |
说 明 |
SOCK_STREAM SOCK_DGRAM SOCK_SEQPACKET SOCK_RAW |
字节流套接口 数据报套接口 有序分组套接口 原始套接口 |
socket函数的protocol常值
protocol |
说 明 |
IPPROTO_TCP IPPROTO_UDP IPPROTO_SCTP |
TCP传输协议 UDP传输协议 SCTP传输协议 |
socket函数中family和type参数的组合
2.connect函数
TCP客户用connect函数来建立与TCP服务器的连接
#include <sys/socket.h> int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen); //返回:若成功则为0,若出错则为-1
sockfd是socket函数返回的套接字描述符,剩下的2个参数分别是一个指向套接字地址结构的指针和该结构的大小。connect函数将激发TCP的三次握手过程,而且仅在连接建立成功或出错时才返回,其中出错有如下几种情况:
1).若TCP客户没有收到SYN包的响应,则返回ETIMEDOUT错误。如调用该函数时,内核发送一个SYN,若无响应则等待6s后再发一个,若仍无响应,则等待24s再发一个,若总共等了75s后仍未收到响应消息则返回该错误(因内核而异)。
2).若响应时RST,表明该服务器主机在我们指定的端口上没有进程等待,客户收到RST包后马上返回ECONNREFUSED错误。
3).若客户发出的SYN在中间的路由器上引发了一个“destination unreachable”的ICMP错误,则按第一种情形继续发送SYN,若在规定的时间内没有收到回应,则将ICMP错误作为EHOSTUNREACH或ENETUNREACH错误返回。
3.bind函数
bind函数把一个本地协议地址赋予一个套接字。对于网际协议,协议地址是一个ip地址和一个端口号
#include <sys/socket.h> int bind (int sockfd, const struct sockaddr *myaddr, socklen_t addrlen); //返回,成功为0,出错为-1
参数sockfd是socket函数返回的套接字描述符,myaddr是一个指向特定于协议的地址结构的指针,第三个参数是该地址结构的长度,对于TCP,调用bind函数可以指定一个端口,或者指定一个地址,也可以两者都指定,还可以都不指定:
- 服务器在启动时候捆绑他们众所周知的端口
- 进程可以把一个特定的IP地址捆绑到它的套接字上,不过这个IP地址必须属于其所在主机的网络接口之一
其中对于IPv4来说,通配地址常值INADDR_ANY来指定,其值一般为0,它通知内核选择IP地址
4.listen函数
#include <sys/socket.h> int listen (int sockfd, int backlog); //返回,成功为0,出错-1
要理解backlog参数,我们要知道内核为任何一个给定的监听套接字维护2个队列:
1).未完成连接队列。客户和服务器之间的tcp三次握手并未完成。
2).已完成连接队列。tcp的三次握手已经完成,处于ESTABLISHED状态。
关于两个队列的处理:
- listen函数的backlog参数曾被规定为两个队列总和的最大值
- 源自Berkeley的实现给backlog增设了一个模糊因子,把它乘以1.5得到未处理队列最大长度
- 不要把backlog定义为0,因为不同的实现对此有不同的解释
- 在三路握手正常完成的前提下(也就是说没有丢失分节,从而没有重传),未完成连接队列的任何一项在其中的存留时间就是一个RTT,而RTT的值取决于特定的客户与服务器
- 当一个客户SYN到达时,若这些队列是满的,TCP就忽略该分节,也就是不发送RST
- 在三路握手完成后,但在服务器调用accept之前到达的数据应由服务器TCP排列,最大数据量为相应已连接套接字的接受缓冲区大小
5.accept函数
accept函数由TCP服务器调用,用于从已完成连接队列列头返回下一个已完成连接,如果已完成连接队列为空,进程将被投入睡眠(如果套接字为默认的阻塞方式)
#include <sys/socket.h> int accept (int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen); //返回:若成功为非负描述符,出错为-1
参数cliaddr和addrlen返回已连接的客户的协议地址,如果对客户的协议地址不感兴趣,可以置为空,参数addrlen在函数调用的时候是传入的套接字地址结构的大小,函数返回时它的值是内核存放在该套接字地址结构中的确切字节数。
如果accept成功,那么其返回值是由内核自动生成的一个全新描述符,代表与返回客户的TCP连接,一般我们称accept函数第一个参数为监听套接字描述符(由socket创建,随后用作bind和listen的第一个参数的描述符),称它的返回值为已连接套接字描述符
accept 函数最多返回三个值:一个既可能是新的套接字描述符也可能是出错指示的整数、客户进程的协议地址(由cliaddr指针所指)、以及该地址的大小(由addrlen指针所指)。
6.fork和exec函数
fork函数(包括有些系统可能提供的它的各种变体)是Unix中派生新进程的唯一方法。
#include <unistd.h> pid_t fork(void); //返回:在子进程中为0,在父进程中为子进程的ID,若出错为-1
理解fork的最难之处在于调用一次,它却反回两,返回值本身告知当前进程是子进程还是父进程。
fork 在子进程中返回0,在父进程中返回子进程的ID号的原因在于:一个子进程只有一个父进程,而且在子进程中可以通过调用getppid获取父进程ID。但是父进程可以有多个子进程,并且在父进程中没有办法获取子进程的ID,如果父进程想跟踪子进程,那么它必须在fork返回后保存子进程的ID。
fork函数的2个典型用法:
- 一个进程创建一个自身的副本,每个副本执行各自的操作。
- 一个进程想要执行另外一个程序,那么它先调用fork函数创建一个自身的副本,然后调用exec函数把自身替换成新的程序。
下面exec函数之间的区别在于
a.待执行的程序是由文件名还是由路径名指定
b.新程序的参数是一一列出还是由一个指针数组来引用
c.把调用进程的环境传递给新程序还是给新城粗指定新的环境
#include <unistd.h> int execl (const char *pathname, const char *arg0, ... /* (char *) 0 */ ); int execv (const char *pathname, char *const argv[]); int execle (const char *pathname, const char *arg0, ... /* (char *) 0, char *const envp[] */ ); int execve (const char *pathname, char *const argv[], char *const envp[]); int execlp (const char *filename, const char *arg0, ... /* (char *) 0 */ ); int execvp (const char *filename, char *const argv[]);
另外进程在调用exec之前打开着的描述符通常跨exec继续保持打开。
7.并发服务器
下面仅给出accept到fork期间C/S的状态
accept返回前客户/服务器的状态
accept返回后客户/服务器的状态
fork返回后客户/服务器的状态
注意,此时listen和connfd这两个描述符都在父进程和子进程共享
在下一步是由父进程关闭已连接套接字,由子进程关闭监听套接字
父子进程关闭相应套接字后客户/服务器的状态
8.close函数
通常的Unix close函数也用来关闭套接字,并终止TCP连接
#include <unistd.h> int close (int sockfd); //返回:若成功为0,出错为-1
close 一个TCP套接字的默认行为是把该套接字设置成已关闭,然后立即返回到调用进程,在并发服务器中,fork一个子进程会复制父进程在fork之前创建的所有描述符,复制完成后相应描述符的引用计数会增加1,调用close 会使描述符的引用计数减1,一旦描述符的引用计数为0,内核就会关闭该套接字。调用close后套接字的描述符引用计数仍然大于0的话,就不会引发TCP的终止序列。如果想在一个TCP连接上发送FIN 可以调用shutdown函数。
9.getsockname和getpeername函数
getsockname函数返回与某个套接字关联的本地协议地址,getpeername函数返回某个套接字关联的外地协议地址
#include <sys/socket.h> int getsockname(int sockfd, struct sockaddr *localaddr, socklen_t *addrlen); int getpeername(int sockfd, struct sockaddr *peeraddr, socklen_t *addrlen); //返回值:成功返回0,出错返回-1.
需要这两个函数,有如下的理由:
1).在一个没有调用bind的TCP客户上,connect成功返回后,getsockname用于返回内核赋予该连接的本地IP地址和本地端口号。
2).在以端口号0调用后,getsockname用于返回内核赋予的本地端口号
3).一旦连接建立,获取客户身份便可以调用getpeername。
4).在一个以通配IP地址调用bind的TCP服务器上,与某个客户的连接一旦建立(accept成功返回),getsockname就可以用于返回由内核赋予该连接的本地IP地址,在这样的调用中,套接字描述符参数必须是已连接套接字的描述符,而不是监听套接字的描述符
5).当一个服务器是由调用过accept的某个进程通过调用exec执行程序时,它能够获取客户身份的唯一途径便是调用getpeername
下图的inet派生就是一例,注意其中子进程是内存映像被替换成新的Telnet服务器的程序文件,也就是说包含对端地址的那个套接字地址结构就此丢失,不过那个已连接套接字描述符跨exec继续保持开放(因为父子进程的拷贝作用?)
10.小结
1).所有的客户服务器程序都是从调用socket函数开始。
2).客户程序的调用顺序一般是 socket --->connect ---->process user input;
3).服务器的调用顺序一般是:socket--->bind--->listen--->accept--->process user input;
4).并发服务器为每个客户连接创建一个进程或者线程来处理客户的请求。
如下图: