本章讲解编写一个完整的TCP客户/服务器程序所需要的基本套接字函数。
socket函数
#include <sys/socket.h> int socket(int family,int type,int protocol); //返回:成功则为非负描述符,若出错则为-1
family参数指明协议族,它是如下某个常值
type参数指明套接字类型,它是如下某个常值
protocol参数为下面某个协议类型常值,或者设为0,以选择所给定family和type组合的系统默认值
下图展示了基本TCP客户/服务器程序的套接字函数
connect函数
TCP客户用connect函数来建立与TCP服务器的连接
#include <sys/socket.h> int connect(int sockfd,const struct sockaddr *servaddr,socklen_t addrlen) //返回:若成功则为0,若出错则为-1
sockfd是由socket函数返回的套接字描述符,第二个、第三个参数分别是一个指向套接字地址结构的指针和该结构的大小。
bind函数
bind函数把本地协议地址赋予一个套接字
#include <sys/socket.h> int bind(int sockfd,const struct sockaddr *myaddr,socklen_t addrlen); //返回:若成功则为0,若出错则为-1
listen函数
listen函数仅由TCP服务器调用,它做两件事情。
1.当socket函数创建一个套接字时,它被假设为一个主动套接字(将调用connect发起连接的客户端套接字)
listen函数把未连接的套接字转换成一个被动套接字,指示内核应接受该套接字的连接请求。
2.本函数的第二个参数规定了内核应该为相应套接字排队的最大连接个数。
#include <sys/socket.h> int listen(int sockfd,int backlog); //返回:若成功则为0,若出错则为-1
本函数通常应该在调用socket和bind这两个函数之后,并在调用accept函数之前调用。
accept函数
accept函数由TCP服务器调用,用于从已完成连接队列队头返回下一个已完成连接。如果已完成连接队列为空,那么进程被投入睡眠。
#include <sys/socket.h> int accept(int sockfd,struct sockaddr *cliaddr,socklen_t *addrlen); //返回:若成功则为非负描述符,若出错则为-1
参数cliaddr和addrlen用来返回已连接的对端进程(客户)的协议地址。
如果accept成功,那么其返回值是由内核自动生成的一个全新描述符,代表与所返回客户的TCP连接。
在讨论accept函数时,我们称它的第一个参数为监听套接字(由socket创建,随后用作bind和listen的第一个参数的描述符),称它的返回值为已连接套接字描述符。
下面程序演示了上面全部函数的用法,这个服务器是一个迭代服务器。是一个显示客户IP地址和端口号的时间获取服务器程序。
1 #include "unp.h" 2 #include <time.h> 3 4 int 5 main(int argc, char **argv) 6 { 7 int listenfd, connfd; 8 socklen_t len; 9 struct sockaddr_in servaddr, cliaddr; 10 char buff[MAXLINE]; 11 time_t ticks; 12 13 listenfd = Socket(AF_INET, SOCK_STREAM, 0); 14 15 bzero(&servaddr, sizeof(servaddr)); 16 servaddr.sin_family = AF_INET; 17 servaddr.sin_addr.s_addr = htonl(INADDR_ANY); 18 servaddr.sin_port = htons(13); /* daytime server */ 19 20 Bind(listenfd, (SA *) &servaddr, sizeof(servaddr)); 21 22 Listen(listenfd, LISTENQ); 23 24 for ( ; ; ) { 25 len = sizeof(cliaddr); 26 connfd = Accept(listenfd, (SA *) &cliaddr, &len); 27 printf("connection from %s, port %d\n", 28 Inet_ntop(AF_INET, &cliaddr.sin_addr, buff, sizeof(buff)), 29 ntohs(cliaddr.sin_port)); 30 31 ticks = time(NULL); 32 snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks)); 33 Write(connfd, buff, strlen(buff)); 34 35 Close(connfd); 36 } 37 }
fork和exec函数
fork函数是UNIX中派生新进程的唯一方法
#include <unistd.h> pid_t fork(void); //返回:在子进程中为0,在父进程中为子进程ID,若出错则为-1
存放在硬盘上的可执行程序文件能够被UNIX执行的唯一方法是现有调用6个exec函数的某一个
#include <unistd.h> int execl(const char *path, const char *arg, ...); int execlp(const char *file, const char *arg, ...); int execle(const char *path, const char *arg,..., char * const envp[]); int execv(const char *path, char *const argv[]); int execvp(const char *file, char *const argv[]); int execvpe(const char *file, char *const argv[],char *const envp[]); int fexecve(int fd,char *const argv[],char *const envp[]);
exec把当前进程映像替换成新的程序文件,而且该新程序通常从main函数开始执行。
关于fork和exec函数可以查看之前写的apue的文章:http://www.cnblogs.com/runnyu/p/4638913.html
并发服务器
上面有一个迭代服务器的例子,对于像时间获取这样的简单服务器来说,这就够了。
然后当服务一个客户请求可能花费较长时间时,我们并不希望整个服务器被单个客户长期占用,而是希望同时服务多个客户。
UNIX编写并发服务器程序最简单的办法就是fork一个子进程来服务每个客户,下面给出了一个典型的并发服务器程序的轮廓。
1 pid_t pid; 2 int listenfd,connfd; 3 listenfd=Socket(...); 4 /* fill in sockaddr_in{} with server'swell-known port */ 5 Bind(listenfd,...); 6 Listen(listenfd,LISTENQ); 7 for(;;) 8 { 9 connfd=Accept(listenfd,...); /* probably blocks */ 10 if((pid=Fork())==0) 11 { 12 Close(listenfd); /* child closes listening socket */ 13 doit(connfd); /* process the request */ 14 Close(connfd); /* done with this client */ 15 exit(0); /* child terminates */ 16 } 17 Close(connfd) /* parent closes connected socket */ 18 }
当一个连接建立时,accept返回,服务器接着调用fork,然后由子进程服务客户(通过已连接套接字connfd),父进程等待另一个连接(通过监听套接字listenfd)
close函数
通常的UNIX close函数也可用来关闭套接字。
#include <unistd.h> int close(int sockfd); //返回:若成功则为0,若出错则为-1
描述符引用计数
在上面并发服务器框架中,父进程关闭已连接套接字只是导致相应描述符的应用计数值减1。
既然引用计数值大于0,这个close调用并不引发TCP的四分组连接终止序列,知道子进程使用close关闭该套接字(引用次数为0)才终止TCP连接。
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