赞
踩
4.1 概述
本章讲解编写一个完整的TCP客户/服务器程序所需要的基本套接字函数,
为下一章编写客户/服务器程序及其扩展做准备。
基本TCP客户/服务器程序的而套接字调用流程图如下:
4.2 socket函数
为了执行网络I/O,一个进程必须做的第一件事情就是调用socket函数
- #include <sys/socket.h>
- int socket(int family, int type, int protocol);
socket函数在成功时返回一个小的非负整数值,它与文件描述符类似,
我们把它称为套接字描述符(socket descriptor),简称sockfd。
其中
family参数指明协议族
family | 说明 |
---|---|
AF_INET | IPv4协议 |
AF_INET6 | IPv6协议 |
AF_LOCAL | Unix域协议 |
AF_ROUTE | 路由套接字 |
AF_KEY | 秘钥套接字 |
type参数指明套接字类型
type | 说明 |
---|---|
SOCK_STREAM | 字节流套接字 |
SOCK_DGRAM | 数据包套接字 |
SOCK_SEQPACKET | 有序分组套接字 |
SOCK_RAW | 原始套接字 |
protocol参数指明使用的协议
protocol | 说明 |
---|---|
IPPROTO_TCP | TCP传输协议 |
IPPROTO_UDP | UDP传输协议 |
IPPROTO_SCTP | SCTP传输协议 |
4.3 connec函数
TCP客户用connect函数来建立与TCP服务器的连接。
- #include <sys/socket.h>
- int connect(int sockfd, const struct sockaddr *servaddr, sockeln_t addrlen);
sockfd是由socket函数返回的套接字描述符,第二个、第三个参数分别是一个指向套接字地址结构的指针和该结构的大小。
套接字地址结构必须含有服务器的IP的地址和端口号
客户在调用函数connect前不必非得调用bind函数,因为如果需要的话,内核会确定源IP地址,并选择一个临时端口作为源端口。
4.4 bind函数
bind函数把一个本地协议地址赋予一个套接字。对于网际协议,
协议地址是32位的IPv4地址或128位的IPv6地址与16位的TCP或UDP端口号的组合。
- #include <sys/socket.h>
- int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
- //返回:成功为0,出错为-1
第二个参数是一个指向特定于协议的地址结构的指针,第三个参数是该地址结构的长度
对于TCP,调用bind函数可以指定一个端口号,或指定一个IP地址,也可以两者都指定,还可以都不指定。
4.5 listen函数
listen函数仅由TCP服务器调用,它做两件事情
(1)将socket函数创建的主动套接字转换成一个被动套接字,listen导致套接字由CLOSE状态转换到LISTEN状态
(2)通过第二个参数指定内核应该为该套接字排队的最大连接个数
- #include <sys/socket.h>
- int listen(int sockfd, int backlog); //成功返回0,出错返回-1
4.6 accept函数
accept函数由TCP服务器调用,用于从已完成连接队列队头返回下一个已完成连接。
如果已完成连接队列为空,那么进程被投入睡眠(假定套接字为默认的阻塞方式)。
- #include <sys/socket.h>
- int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
- // 成功返回非负描述符,出错返回-1
4.7 fork和exec函数
fork函数是Unix中派生新进程的唯一方法。
- #include <unistd.h>
- pid_t fork(void);
调用fork函数,在子进程中返回0,父进程中返回子进程的进程ID号。
fork有两个典型用法。
(1)一个进程创建一个自身的副本,这样每个副本都可以在另一个副本执行其他任务的同时处理各自的某个操作。
这是网络服务器的典型用法。
(2)一个进程想要执行另一个程序。既然创建新进程的唯一办法是调用fork,该进程于是首先调用fork函数创建一个自身的副本,然后其中一个副本(通常为子进程)调用exec,把自身替换成新的程序,这是诸如shell之类程序的典型用法。
4.8 并发服务器
当服务一个客户请求可能花费较长时间时,我们并不希望整个服务器被单个客户长期占用,
而希望同时服务多个客户。Unix中编写并发服务器程序最简单的办法就是fork一个子进程来服务每个客户。
- // 典型的并发服务器程序轮廓
- pid_t pid;
- int listenfd, connfd;
- listenfd = Socket( ... );
- /* fill in sockaddr_in{} with server's well-known port */
- Bind(listenfd, ... );
- Listen(listenfd, LISTENQ);
- for ( ; ; )
- {
- connfd = Accept(listenfd, ... ); /* probably blocks */
- if ( (pid = Fork()) == 0)
- {
- Close(listenfd); /* child closes listening socket */
- doit(connfd); /* process tht request */
- Close(connfd); /* done with this client */
- exit(0); /* child terminates */
- }
- Close(connfd); /* parent closes connected socket */
- }

调用fork后,对connfd的引用计数由1变为2,所以在父进程中Close(connfd)关闭父进程已连接套接字,会使2变为1,
并不会导致与客户的连接断开。
此外,我们在子进程中显式调用close也不是必须的,因为在调用exit后,进程终止处理的部分会关闭所有由内核打开的描述符。
4.9 close函数
close一个TCP套接字的默认行为是把该套接字标记成已关闭。
如果该描述符的引用计数大于1,则会使其引用计数减1。如果减1后依然大于0,则不会引发TCP的四分组连接终止序列。
如果我们确实像在某个TCP连接上发送一个FIN,可以改用shutdown函数代替close。
4.10 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);
4.11 小结
大多数TCP服务器是并发的,他们为每个待处理的客户连接调用fork派生一个子进程。
大多数UDP服务器是迭代的。
尽管这两个模型已经成功地运用了许多年,我们仍将在之后探讨使用线程和进程的其他服务器程序设计方法。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。