unp第三章主要介绍了基本套接字编程函数。主要有:socket(),bind(),connect(),accept(),listen()等。
本博文也直接进入正题,对这几个函数进行剖析和讲解。
1. 基本套接字函数
在《计算机网络》和《TCP/IP详解》中,我们经常讨论TCP/IP的工作流程,连接建立的三次握手和连接断开的四次挥手等,那么这些如何体现在程序中呢?我们如何来运用这些理论知识于实践之中呢?下面我们来看看套接字编程中客户和服务器进程之间的一些典型事件的时间表。
如图,服务器首先启动,稍后客户进程启动,它通过connect()函数试图连接服务器,这个阶段完成三次握手,然后read()和write()完成客户和服务器之间的数据传输,之后客户进程调用close()来请求断开连接,服务器收到后读取EOF,接着关闭连接,这时完成四次挥手的过程。下面就图中的每个函数,细细剖析他们的用途。
2. socket函数
为了执行网路I/O,进程做的第一件事情就是调用socket()函数,指定期望的通信协议类型。
#include <sys/socket.h>
/* family --指明协议簇 */
/* type --指明套接字类型 */
/* protocol --指明使用那个协议(当此项为0时,则根据family和type组合的系统默认值) */
int socket (int family, int type, int protocol);//若成功则返回非负描述符sockfd,若出错则返回-1
下表为他们的一些取值:
family | 说明 | type | 说明 | protocol | 说明 | ||
---|---|---|---|---|---|---|---|
AF_INET | IPv4协议 | SOCK_STREAM | 字节流套接字 | IPPROTO_TCP | TCP传输协议 | ||
AF_INET6 | IPv6协议 | SOCK_DGRAM | 数据报套接字 | IPPROTO_UDP | UDP传输协议 | ||
AF_LOCAL | Unix域协议 | SOCK_SEQPACKET | 有序数组套接字 | IPPROTO_SCTP | SCTP传输协议 | ||
AF_ROUTE | 路由套接字 | SOCK_RAW | 原始套接字 | ||||
AF_KEY | 密钥套接字 |
当然,这些参数不能随便设置的,下表给出了一些有效的组合和对应真正的协议
Protocol | AF_INET | AF_INET6 | AF_LOCAL | AF_ROUTE | AF_KEY |
---|---|---|---|---|---|
SOCK_STREAM | TCP/SCTP | TCP/SCTP | 是 | ||
SOCK_DGRAM | UDP | UDP | 是 | ||
SOCK_SEQPACKET | SCTP | SCTP | 是 | ||
SOCK_RAW | IPv4 | IPv6 | 是 | 是 |
3. connect函数
client客户进程调用此函数来建立与服务器间的连接。
#include <sys/socket.h>
/* sockfd --socket()函数返回的套接字描述符 */
/* servaddr --指向套接字地址结构的指针 */
/* addrlen --该结构的大小 */
int connect (int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);//若成功返回0,反之返回-1
客户在调用connect()时没必要调用bind(),因为如果需要的话,内核会确定源IP地址,并选择一个临时端口作为源端口。
调用connect()函数,出错的情况有如下几种:
若TCP客户没有接收到SYN分节的响应,则返回ETIMEOUT错误。
若对客户的响应时RST(复位),则表明该服务器主机在我们制定的端口上没有进程在等待与其连接,会返回ECONNREFUSED。如服务器进程也许没有运行的情况。(硬错误)
RST是TCP在发生错误时发送的一个TCP分节。产生RST的三个条件:目的地为某端口的SYN到达,然而在该端口没有正在监听的服务器;TCP想取消一个已有连接;TCP接收到一个根本不存在的连接上的分节。
- 若客户发出的SYN在中间某个路由器上引发“目的地不可达的ICMP错误”,则认为时一种“软错误”,按照TCP协议的规定,客户会按照一定的时间间隔重发SYN,若在某个规定时间内仍未收到响应,则返回EHOSTUNREACH和ENETUNREACH错误。
4. bind函数
用于把一个本地协议地址赋予一个套接字。对于网际网协议,协议地址时32位的IPv4地址或者128位IPv6地址与16位的TCP或UDP端口号的组合。调用bind可以制定IP地址或端口,也可以两者都指定,也可以都不指定。
#include <sys/socket.h>
/* sockfd --socket()函数返回的套接字描述符 */
/* servaddr --指向套接字地址结构的指针 */
/* addrlen --该结构的大小 */
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);//若成功返回0,反之返回-1
5. listen函数
该函数主要干两件事情
(1) 当socket函数创建一个套接字时,它被假设为一个主动套接字(将调用connect发起连接的客户套接字)
listen函数把一个未连接的套接字转换成一个被动套接字,指示内核应接受指向该套接字的连接请求。
(2) 本函数的第二个参数规定了内核应该为相应套接字排队的最大连接个数
#include <sys/socket.h>
/* sockfd --socket()函数返回的套接字描述符 */
/* backlog --内核为相应套接字排队的最大连接个数 */
int listen(int sockfd, int backlog);//若成功返回0,反之返回-1
6. accept函数
此函数由TCP服务器调用,用于已完成连接队列对头返回下一个已完成连接。如果已完成连接队列为空,那么进程被投入睡眠。
/* sockfd --socket()函数返回的套接字描述符 */
/* cliaddr --用来返回已连接的对端进程的协议地址 */
/* addrlen --调用前:为cliaddr所指的套接字地址结构大小 */
/* --调用后:为由内核存放在该套接字地址结构内的确切字节数 */
int accept(int sockfd, struct sockaddr *cliaddr , socklen_t *addrlen);//若成功返回非负描述符,若出错,则返回-1
如果accept调用成功,则返回一个由内核自动生成的全新描述符,代表与所返回客户的TCP连接。
accept的参数socket通常被称为监听套接字描述符,而其返回值为连接套接字描述符。一个服务器通常只创建一个监听套接字描述符,内核为每个由服务器进程接受的客户创建一个已连接套接字(三次握手已完成)。
7. 一个简单的值-结果例子
#include "unp.h"
#include <time.h>
int
main(int argc, char **argv)
{
int listenfd, connfd;
socklen_t len;
struct sockaddr_in servaddr, cliaddr;
char buff[MAXLINE];
time_t ticks;
//创建套接字
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
//初始化套接字
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;//IPv4协议
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//通配地址,一般为0
servaddr.sin_port = htons(13);//时间服务端口
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
for ( ; ; ) {
len = sizeof(cliaddr);
connfd = Accept(listenfd, (SA *) &cliaddr, &len);
printf("connection from %s, port %d\n",
Inet_ntop(AF_INET, &cliaddr.sin_addr, buff, sizeof(buff)),
ntohs(cliaddr.sin_port));
ticks = time(NULL);
snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
Write(connfd, buff, strlen(buff));
Close(connfd);
}
}
编译执行上述代码,即可运行服务器端程序,那么我们开启客户端程序连接服务器,就会输出客户IP地址和端口号。
注意:此处如果出现bind error:Address already in use!,可参考:【unix网络编程第三版】ubuntu端口占用问题
# ./daytimes
connection from 127.0.0.1, port 53458
connection from 192.168.191.2, port 54358
8. fork和exec函数
fork函数是Unix中派生新进程的唯一方法。
#include<unistd.h>
pid_t fork(void);/* 在子进程中返回0,在父进程中为子进程ID,若出错则返回-1
任何子进程只有一个父进程,而且父进程通过getppid取得父进程ID,而父进程可以有许多子进程,而且无法获取各个子进程的进程ID,如果父进程想要知道子进程的ID,只能通过记录每次调用fork的返回值。
fork的两个典型用法:
(1) 一个进程创建一个自身的副本,这样每个副本都可以在另一个副本执行其他任务的同时处理各自的某个操作。
(2) 一个进程想要执行另一个进程。fork函数创建一个副本,然后通过调用exec把其中一个副本替换成新的程序。
存放在硬盘你上的可执行程序文件能够被Unix执行的唯一方法:由一个现有的进程调用六个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 execve(int fd,char *const argv[],char *const envp[]);
9. 并发服务器
像上面提到的时间获取服务器,属于迭代服务器。这种服务器都被一个单一客户占用,如果客户请求时间长,则该服务器被长期占用,这显然会影响效率。所以,我们希望服务器能尽可能同时服务多个用户。这种服务器称为并发服务器。
并发服务器利用fork函数创建一个子进程来服务客户。
int
main(int argc, char **argv)
{
pid_t pid;
int listenfd, connfd;
socklen_t len;
struct sockaddr_in servaddr;
time_t ticks;
//创建套接字
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
//初始化套接字
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;//IPv4协议
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//通配地址,一般为0
servaddr.sin_port = htons(13);//时间服务端口
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
for ( ; ; ) {
connfd = Accept(listenfd, (SA *) &cliaddr, &len);
if((pid = fork())==0)
{
close(listenfd);
doit(connfd);
close(connfd);
exit();
}
Close(connfd);
}
}
分析以上程序:
父进程:pid为子进程ID,不为0,则将connfd的引用套接字减1,父进程继续等待下一个客户连接
子进程:fork函数之后,监听套接字和已连接套接字的引用技术都加1,pid==0,首先监听套接字listenfd的引用计数减1(不会关闭监听套接字),然后执行客户所需的操作(doit),再关闭connfd(引用计数减1,此时为0)。子进程处理客户需求结束,exit关闭进程。
10 close函数
用来关闭套接字,并中止TCP连接。
#include <unistd.h>
int close(int sockfd);/* 若成功则返回0,出错则返回-1*/
close函数调用后只是将引用计数减1,只有当引用技术为0时,才会测地关闭该套接字,清理和资源释放。
11 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);
需要使用上述函数的情况如下:
(1) 在一个没有调用bind的TCP客户上,connect成功返回后,getsockname用于返回由内核赋予该连接的本地IP地址和本地端口号
(2) 在以端口0调用bind后,getsockname用于返回由内核赋予的本地端口号
(3) getsockname用于获取某个套接字的地址族
(4) 以通配IP地址调用bind的服务器上,与客户一旦建立连接,getsockname可用于返回由内核赋予该连接的本地IP地址
(5) 在一个服务器是由调用过accept的某个进程通过调用exec执行程序时,它只能通过getpeername来获取客户的IP和端口号