资源描述
第10章 套接字编程基础
网络编程有两种主要的编程接口,一种是Berkeley UNIX(用于BSD UNIX)的套接字编程接口,另一种是AT&T的TLI接口(用于UNIX System V)。本章主要介绍Berkeley 套接字编程接口的基本结构、常用的调用和程序以及使用方法。
10.1 套接字概述
20世纪80年代早期,美国国防部高级研究计划署给加利福尼亚大学伯克利分校提供了资金,在UNIX系统上首次实现了TCP/IP。Socket编程界面是由4BSD UNIX首先提出,目的是解决互联网的进程通信问题。由于越来越多的计算机厂商采用了Berkeley UNIX,套接字接口被广泛认可并广泛采用,成为事实上的工业标准。目前的SYSV,BSD,OSF都将套接字接口作为系统的一部分。在当时设计如何支持TCP/IP协议时,有两种加入函数的方法,一种是直接加入支持TCP/IP协议的调用,另一种是加入支持一般网络协议的函数,而用参数来指定支持TCP/IP协议。Berkeley采用了后者,这样可以支持多协议族。
10.1.1 套接字描述符
套接字使用UNIX文件描述符 (file descriptor) 作为与其他程序通信的方式。
UNIX程序在执行任何形式的I/O的时候,程序是在读写一个文件描述符。一个文件描述符只是一个和打开的文件相关联的整数。这个文件可能是一个网络连接、队列、管道、终端、磁盘上的文件或者其他的设备。
每个进程都有一个文件描述符表,该表中存放打开的文件描述符。用户使用open()等函数得到的文件描述符其实是文件描述符在该表中的索引号,该表项的内容是一个指向文件表的指针。应用程序只要使用该描述符就可以对指定文件进行操作。
同样,套接字接口增加了网络通信操作的抽象定义,与文件操作一样,每个打开的套接字都对应一个整数,称它为套接字描述符,该整数也是套接字描述符在文件描述符表中的索引值。但套接字描述符在描述符表中的表项并不指向文件表,而是指向一个与该套接字有关的数据结构。BSD UNIX中新增加了一个socket()函数,应用程序可以调用它来新建一个套接字描述符,完成建立通信的初步工作,一旦建立了一个套接字,应用程序可以使用其他特定的调用来为它添加其他详细信息,以完成建立通信的过程。
socket实质上提供了进程通信的端点,进程通信之前双方必须各自创建一个端点。在互联网内部每一个socket用一个半相关描述:
{协议,本地地址,本地端口}
一个完整的socket连接则用一个相关描述:
{协议,本地地址,本地端口,远地地址,远地端口}
每一个socket有一个本地唯一的socket号,由操作系统分配。
Socket是面向客户/服务器模型而设计的,针对客户和服务器程序提供不同的socket调用,客户随机申请一个socket,系统为之分配一个socket号,服务器拥有全局公认的socket,客户可以向它发出连接请求和信息请求。
10.1.2 客户―服务器模式
网络编程中最常见的是客户-服务器模式。以该模式编程时,服务器端有一个进程(或多个进程)首先启动,在指定的端口上监听,等待客户端的程序发来请求,客户端在需要时向服务器端发出的连接请求。一旦连接上之后,就可以按设计的数据交换方法和格式进行数据传输。在使用TCP协议时,一般服务器端进程先使用socket()函数调用得到一个描述符,然后使用bind()函数将一个名字与套接字描述符连接起来,对于Internet域就是将Internet地址绑定到套接字。之后,服务器端使用listen()函数指出等待服务请求队列的长度。然后就可以使用accept()函数等待客户端发起连接(一般以阻塞方式等待连接,也有使用非阻塞的方式),一旦有客户端发出请求,accept()函数返回客户端的地址信息,并返回一个新的套接字描述符,该描述符与原先的套接字有相同的特性,这时服务器端就可以使用这个新的套接字进行读写操作。一般服务端可能在accept()函数返回后创建一个新的进程与客户进行通信,父进程则再到accept()函数处等待另一个连接。客户端进程一般先使用socket()函数得到一个套接字描述符,然后使用connect()函数向指定的服务器上的指定端口发起连接,一旦连接成功返回,就说明已经建立了与服务器的连接,这时就可以通过套接字描述符进行读写操作了。图10.1是在客户端和服务器端使用TCP时,客户进程和服务器进程套接字建立连接的过程。
图10.1 面向连接的客户─服务器模型时序图
使用无连接的UDP协议时,服务器端进程先创建一个套接字,之后调用recvfrom()函数接收客户端的数据报,然后调用sendto()函数将要返回客户端的消息发送给客户进程。客户端也要先创建一个套接字,再使用sendto()函数向服务器端进程发出请求,使用recvfrom()函数得到返回的消息。
图10.2 无连接的客户─服务器模型时序图
10.2 TCP初等网络函数
Linux系统是通过提供套接字来进行网络编程的。网络应用程序通过套接字和其他几个函数的调用,返回一个通信的文件描述符,可以将这个描述符看成普通的文件的描述符,通过向描述符读写操作以实现网络之间的数据交流。
10.2.1 服务器端的函数
1. 建立服务socket()函数
服务器要能够为客户端服务,必须提供服务程序。提供服务程序的过程首先必须先建立套接字以监听客户端的请求。socket()函数就完成这个工作。
socket( )调用格式如下:
sockid = socket ( af, type, protocol)
返回值sockid是一个整数,即socket号。因此,创建一个socket实际上是向系统申请一个属于自己的socket号,socket( )一共有三个参数。
其中:
af(address family):地址族。指出本socket所用的地址类型,TCP/IP地址族为AF_INET
type:类型。指创建socket的应用程序所希望的通信服务类型,
TCP对应的类型为SOCK_STREAM
UDP对应的类型为SOCK_DGRAM
IP对应的类型为SOCK_RAW
Protocol:协议。指本socket所希望的协议,如TCP, UDP, ICMP等。TCP/IP协议族中,协议族与socket类型基本上可以唯一确定一个协议,因此,协议值通常可以置0。
socket系统调用的过程如下:
因为套接字有几种类型,首先要注明要建立什么类型的。还要选择套接字的地址格式。socket() 函数有两个最重要的选项是AF_UNIX和IAF_INET。AF_UNIX就像UNIX路径名一样识别套接字。这种形式对于在同一台机器上的进程间通信很有用。而AF_INET使用如 192.9.200.10 这样被点号隔开的4个十进制数的地址格式。除了机器地址以外,还可以利用端口号来允许每台机器上的多个AF_INET套接字。下面主要介绍广泛使用的AF_INET方式。
另一个必须提供的参数是套接字的类型。两个重要的类型是SOCK_STREAM 和SOCK_DGRAM。SOCK_STREAM 表明数据以字符流方式通过套接字。而SOCK_DGRAM 则表明数据是数据报的形式。下面介绍常见并易于使用的SOCK_STREAM 套接字。其功能是指定协议类型。定义如下。
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type,int protocol)
其中,domain说明网络程序所在的主机采用的通信协议族AF_UNIX和AF_INET等。AF_UNIX只能够用于单一的UNIX系统进程间通信,而AF_INET是面向Internet的,因而可以允许在远程主机之间通信。
Type是网络程序所采用的通信协议SOCK_STREAM,SOCK_DGRAM等。SOCK_STREAM 表明用的是TCP协议,这样会提供按顺序的、可靠的、双向的、面向连接的位流。 SOCK_DGRAM 表明用的是UDP协议,这样只会提供定长的、不可靠的、无连接的通信。
Protocol,由于指定了类型,所以一般只要用0来代替就可以了。
返回值:出错时返回-1,成功时返回套接字描述符sockfd 。
socket()函数指定了协议族(IPv4,IPv6或UNIX)和套接字类型(字节流、数据报或原始套接字),但并没有指定本地协议地址或远程协议地址。
socket 为网络通信做基本的准备。成功时返回文件描述符,失败时返回-1,由errno可知道出错的详细情况。
2. bind()函数
在建立套接字后,要使用bind() 函数提供套接字监听的地址。
功能:bind( ) 给套接字分配一个本地协议地址。即将本地socket地址(包括本地主机地址和本地端口)与所创建的socket号联系起来,即将本地socket地址赋予socket,其格式为:
bind (sockid, localaddr, addrlen)
其中:sockid,socket号
localddr, 本地socket地址,指定本地主机地址和本地端口号
addrlen, 地址长度,以字节为单位的地址结构的长度
bind( )函数的定义:
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *my_addr, int addrlen)
其中:
sockfd是由socket()函数返回的文件描述符。
Addrlen是sockaddr结构的长度。
my_addr是指向数据结构 struct sockaddr 的指针,保存地址(即端口和 IP 地址) 信息。sockaddr的定义如下。
struct sockaddr{
unisgned short as_family;
char sa_data[14];
}
但由于考虑到系统的兼容性,一般不用这个头文件,而使用另外一个结构struct sockaddr_in 来代替。sockaddr_in的定义如下。
struct sockaddr_in{
unsigned short sin_family;
unsigned short int sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
{
在Internet的编程中,sin_family一般为AF_INET,sin_addr设置为INADDR_ANY表示可以和任何的主机通信,sin_port是要监听的端口号,sin_zero[8]是用来填充的。
bind将本地的端口同套接字返回的文件描述符捆绑在一起。该函数返回0表示成功,返回-1表示出错。
addrlen 设置为 sizeof(struct sockaddr)。
要让内核自动处理地址IP和端口port,可进行如下处理。
my_addr.sin_port = 0; /* 让bind( ) 自己选择合适的端口*/
my_addr.sin_addr.s_addr = INADDR_ANY; /*自动填上所运行的机器的 IP 地址*/
3. listen()函数
listen( )函数的功能:将未连接主动套接字的转换为被动套接字,指示内核接受对该套接字的连接请求。
用于面向连接服务器,表示愿意接收连接,listen( )在accept( )之前调用。listen( )函数的调用格式是:
listen (sockid, quelen)
其中:sockid, 本地socket号
quelen, 请求队列长度,listen( )以此参数限制排队请求的个数。
listen( )函数的定义为:
#include <sys/socket.h>
int listen(int sockfd, int backlog);
其中:sockfd是调用 socket() 函数返回的套接字文件描述符,是绑定后的文件描述符。Backlog用于设置请求排队的最大长度,当有多个客户端程序和服务端相连时, 使用这个表示可以介绍的排队长度。Listen()函数将绑定的文件描述符变为监听套接字,返回的情况与bind()函数相同。
下面的代码说明如何利用 socket(),bind() 和 listen() 函数建立连接并可以接受数据。
/* 建立套接字的程序; 源代码由 bzs@bu-cs.bu.edu 提供 */
int establish(unsigned short portnum)
{ char myname[MAXHOSTNAME+1];
int s;
struct sockaddr_in sa;
struct hostent *hp; memset(&sa, 0, sizeof(struct sockaddr_in)); /*本机地址*/
gethostname(myname, MAXHOSTNAME); /*得到本机地址信息*/
hp= gethostbyname(myname);
if (hp == NULL) /*若本机不存在!? */
return(-1);
sa.sin_family= hp->h_addrtype; /*这是本主机的地址 */
sa.sin_port= htons(portnum); /* this is our port number */
if ((s= socket(AF_INET, SOCK_STREAM, 0)) < 0) /*建立套接字*/
return(-1);
if (bind(s,&sa,sizeof(struct sockaddr_in)) < 0) {
close(s);
return(-1); /*将地址绑定到套接字*/
}
listen(s, 3); /* 队列的最大连接数*/
return(s);
}
4. accept ()函数
accept( )用于面向连接的服务器。其功能是在已完成队列头返回下一个已完成的连接。
accept( )函数的调用格式为:
newsock = accept (sockid, clientaddr, addrlen)
其中:sockid, 本地socket号
clientaddr, 指向客户socket地址结构的指针,初始值为空,客户连接上后指向客户地址
addrlen, 客户socket地址长度
accept( )函数的定义:
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *cliaddr, int* addrlen);
其中,sockfd是listen后的文件描述符。Addr,addrlen是用来给客户端的程序填写的,服务器端只要传递指针就可以了。bind()、listen()和accept()是服务器端用的函数,调用accept()函数时,服务器端的程序会一直阻塞到某个客户程序发出了连接为止。 accept成功时返回最后的服务器端的文件描述符,这时服务器端就可以向该描述符写信息了。失败时返回-1。
5. fork()函数
服务器应该为多个用户服务,在处理连接时,应该还可以接受调用。为此,一般用 fork()函数来处理每个连接。
功能:派生新进程。
定义:
#include <sys/unistd.h>
pid_t fork (void);
在子进程中返回0,在父进程中返回子进程的进程ID。
返回码:
出错时返回 -1,调用一次返回两次。
6. fork()函数的典型应用
(1)一个进程可为自己创建一个备份。当一个备份处理一个操作时,其他的备份可以执行其他的任务。这是非常典型的网络服务器。
(2)一个进程想执行其他的程序,由于创建新进程的惟一方法是调用fork()函数,进程首先调用fork()函数以生成一个备份,然后其中一个备份(通常为子进程)调用exec来代替自己去执行新程序。
下面的代码演示了如何使用 establish() 和 get_connection() 函数来处理多个连接。
#include <errno.h> /* 必须的头文件*/
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <netdb.h>
#define PORTNUM 50000 /*需要一个端口数值,50000是一个随意选用的数值*/
void fireman(void);
void do_something(int);
main()
{ int s, t;
if ((s= establish(PORTNUM)) < 0) { /* 建立端口号*/
perror("establish");
exit(1);
}
signal(SIGCHLD, fireman); /* 除去僵进程*/
for (;;) { /* 循环,等待呼叫*/
if ((t= get_connection(s)) < 0) { /* 接受一个连接 */
if (errno == EINTR) /* 如果accept()函数产生EINTR错误, */
continue; /* 重试*/
perror("accept"); /* 还是不成,撤退 */
exit(1);
}
switch(fork()) { /* 试图管理连接*/
case -1 : /* 若无法建立子进程*/
perror("fork"); /* 撤退 */
close(s);
close(t);
exit(1);
case 0 : /* 如果是子进程,则调用 do something函数 */
close(s);
do_something(t);
exit(0);
default : /* 如果是父进程,则查找另一个连接*/
close(t);
continue;
}
}
}
/* 由于子进程死亡,故应得到其返回值或得到其僵进程*/
void fireman(void)
{
while (waitpid(-1, NULL, WNOHANG) > 0)
;
}
/* 这是一个与套接字共同工作的函数,在得到一个连接后调用该函数*/
void do_something(int s)
{
/* 以下是对套接字进行的操作
:
:
*/
}
在建立了套接字后,要等待对该套接字的调用。accept() 函数返回一个新的连接到调用方的套接字。 演示的代码如下。
/*在用establish()函数建立的套接字上等待连接的发生 */
int get_connection(int s)
{ int t; /* 连接的套接字*/
if ((t = accept(s,NULL,NULL)) < 0) /* 如果存在某个连接,则接受 */
return(-1);
return(t);
}
10.2.2 在客户端建立连接
1. 建立客户端套接字
客户端要调用服务器端上的套接字,要先有个套接字。和建立监听的套接字一样,该工作使用 socket() 函数来完成。
2. 用connect() 函数连接
在给出套接字的地址后,可以用 connect() 函数来连接要监听的套接字。connect( )函数的功能是建立与TCP服务器的连接。其调用格式如下:
connect (sockid, dstaddr, addrlen)
其中:sockid, 本地socket号
dstaddr: 一个指向对方socket地址结构的指针
addrlen: 对方socket地址长度
connect() 函数的定义:
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
其中:sockfd 是系统调用 socket() 返回的套接字文件描述符。serv_addr储存了服务器端的连接信息,其中sin_add是服务器端的地址。addrlen serv_addr的长度,设置为 sizeof(struct sockaddr)。
3. 客户端用connect()函数发起连接
服务器端必须通过调用socket()、bind()和1isten()函数来完成接受外来连接的准备,称为“被动打开(Passive Open)”。
客户端通过connect()函数进行主动打开(active open),这引起客户TCP发送一个SYN分节(表示同步),它告诉服务器客户端将在待建立的连接中发送的数据的初始序列号。
服务器端必须确认客户的SYN,同时自己也得发送一个SYN分节,它含有服务器端将在同一连接中发送的数据的初始序列号。服务器端以单个分节向客户端发送SYN和对客户SYN的ACK。客户端必须确认服务器端的SYN。
4. 连接出错时的返回信息
connect()函数是客户端用来同服务器端进行连接的。成功时返回0;sockfd是同服务器端通信的文件描述符,失败时返回-1。
(1)服务器超时错误
出错原因 :未收到SYN的响应(服务器超时,75秒)。
返回值:ETIMEDOUT
用户端输出:Connection time out.
(2)“服务器无此项端口服务”错误
出错原因 :收到RST响应SYN到达服务器,但该服务器无此项端口服务。
返回值:ECONNREFUSE
用户端输出:Connection refused
(3)ICMP错误,目的地不可达
出错原因:ICMP错误,目的地不可达。
返回值:EHOSTUNREACH
用户端输出:No route to host
在给出套接字地址后,可以用 connect() 函数来连接监听的套接字。代码如下。
int call_socket(char *hostname, unsigned short portnum)
{ struct sockaddr_in sa;
struct hostent *hp;
int a, s;
if ((hp= gethostbyname(hostname)) == NULL) { /* 是否知道主机地址? */
errno= ECONNREFUSED;
return(-1); /* 不知道*/
}
memset(&sa,0,sizeof(sa));
memcpy((char *)&sa.sin_addr,hp->h_addr,hp->h_length); /* 设置地址*/
sa.sin_family= hp->h_addrtype;
sa.sin_port= htons((u_short)portnum);
if ((s= socket(hp->h_addrtype,SOCK_STREAM,0)) < 0) /* 得到套接字*/
return(-1);
if (connect(s,&sa,sizeof sa) < 0) { /* 连接 */
close(s);
return(-1);
}
return(s);
}
这个函数返回一个可以通过数据的套接字。
10.2.3 通过套接字传输数据
在传输数据的双方建立连接之后,就可以像处理一般的文件一样,直接使用read() 和 write() 函数来进行读写处理,或传输数据。但通常的文件读写相比,套接字的读写一般不能一次得到所要的所有数据,所以要一直循环到得到所需的全部数据。下面是一个将数据读到缓存的简单例子。
int read_data(int s, / * 已经连接的套接字*/
char *buf, /* 指向缓冲区的指针*/
int n /* 所需要的字符数(bytes) */
)
{ int bcount; /* 读入的字节计数*/
int br; /* 本次应读入的字节数*/
bcount= 0;
br= 0;
while (bcount < n) { /* 循环,直到缓冲区满为止*/
if ((br= read(s,buf,n-bcount)) > 0) {
bcount += br; /* increment byte counter */
buf += br; /* 移动缓冲区指针,以便下一次读入*/
}
else if (br < 0) /* 向调用者发出一个“错误”信号*/
return(-1);
}
return(bcount);
}
相同的函数也可以写数据,留给读者思考。
10.2.4 关闭连接
数据传输完毕之后,需要关闭套接字之间连接。一般可使用 close() 函数关闭每边的套接字连接。如果一边的套接字已经关闭,而另外一边仍在向它写数据,则返回一个错误代码。
套接字功能是将套接字做上“已关闭的标记”,并立即返回进程。将套接字描述字的访问计数器减1。当访问计数器值为0时,引发TCP的四个“分组连接终止”序列,从而关闭套接字。
其定义如下。
#include <sys/unistd.h>
int close(int sockfd);
10.2.5 面向连接的套接字实例
服务器端通过一个连接向客户端发送字符串“Hello,world!”。只要在服务器上运行该服务器软件,在客户端运行客户软件,客户端就会收到该字符串。
1. 服务器端代码实例
/******************************/
/* 服务器端代码程序 (server.c) */
/******************************/
#include stdio.h
#include stdlib.h
#include errno.h
#include string.h
#include sys/types.h
#include netinet/in.h
#include sys/socket.h
#include sys/wait.h
#define MYPORT 3490 /*服务器监听端口号 */
#define BACKLOG 10 /* 最大同时连接请求数 */
main()
{
intsock fd,new_fd; /* 监听套接字:sock_fd,数据传输套接字:new_fd * /
struct sockaddr_in my_addr; /* 本机地址信息 */
struct sockaddr_in their_addr; /* 客户地址信息 */
n_size;
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { /*错误检测 */
perror("socket"); exit(1); }
my_addr.sin_family=AF_INET;
my_addr.sin_port=htons(MYPORT);
my_addr.sin_addr.s_addr = INADDR_ANY;
bzero(&(my_addr.sin_zero),8);
if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockad dr)) == -1) { /*错误检测*/
perror("bind"); exit(1); }
if (listen(sockfd, BACKLOG) == -1) { /*错误检测*/
perror("listen"); exit(1); }
while(1) { /* main accept() loop */
sin_size = sizeof(struct sockaddr_in);
if ((new_fd = accept(sockfd, (struct sockaddr *)&their_addr,
&sin_size)) == -1) {
perror("accept"); continue; }
printf("server: got connection from %s ",
inet_ntoa(their_addr.sin_addr));
if (!fork()) { /* 子进程代码段 */
if (send(new_fd, "Hello, world! ", 14, 0) == -1)
perror("send"); close(new_fd); exit(0); }
close(new_fd); / * 父进程不再需要该套接字*/
waitpid(-1,NULL,WNOHANG) > 0 /*等待子进程结束,清除子进程所占用资源 */
}
} /*end 程序server.c
服务器端首先创建一个套接字,然后将该套接字与本地地址及端口号捆绑,如果成功就在相应的套接字上监听。当accpet()函数捕捉到一个连接服务请求时,就生成一个新的套接字,并通过这个新的套接字向客户端发送字符串“Hello,world!”,然后关闭该套接字。
fork()函数生成一个子进程来处理数据传输部分,fork()语句对于子进程返回的值为0。所以包含fork()函数的if语句是子进程代码部分,它与if语句后面的父进程代码部分是并发执行的。
2. 客户端程序
/******************************/
/* 客户端代码程序 (client.c) */
/******************************/
#include stdio.h
#include stdlib.h
#include errno.h
#include string.h
#include netdb.h
#include sys/types.h
#include netinet/in.h
#include sys/socket.h
#define PORT 3490
#define MAXDATASIZE 100 /*每次最大数据传输量 */
int main(int argc, char *argv[])
{
int sockfd, numbytes;
char buf[MAXDATASIZE];
struct hostent *he;
struct sockaddr_in their_addr;
if (argc != 2) {
fprintf(stderr,"usage: client hostname "); exit(1); }
if((he=gethostbyname(argv[1]))==NULL) {
herror("gethostbyname"); exit(1); }
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket"); exit(1); }
their_addr.sin_family=AF_INET;
their_addr.sin_port=htons(PORT);
their_addr.sin_addr = *((struct in_addr *)he->h_addr);
bzero(&(their_addr.sin_zero),8);
if (connect(sockfd, (struct sockaddr *)&their_addr, sizeof(struct sockaddr)) == -1)
{ /*错误检测*/
perror("connect");
exit(1);
}
if ((numbytes=recv(sockfd, buf, MAXDATASIZE, 0)) == -1) {
perror("recv"); exit(1); }
buf[numbytes] = \;
printf("Received: %s",buf);
close(sockfd);
return 0;
} /*end程序client.c
客户端代码相对来说要简单一些,首先通过服务器域名获得其IP地址,然后创建一个套接字,调用connect()函数与服务器建立连接,连接成功之后接收从服务器发送过来的数据,最后关
展开阅读全文