《第六章UNIX的网络通信初步》由会员分享,可在线阅读,更多相关《第六章UNIX的网络通信初步(107页珍藏版)》请在金锄头文库上搜索。
1、第六章第六章 UNIX的网络通的网络通信初步信初步(1)UNIX操作系统为进程通信提供了相应设施,如管道(pipe)、命名管道(namedpipe)和软中断信号(signal),消息(message)、共享存储区(sharedmemory)和信号量(semaphore)等,但这些都只限于用在本机进程间的通信。(2)为了实现计算机全面联网与信息的异地处理,需要为用户构建Client/Server应用的通讯结构,通过网络接口编程,以解决不同主机进程间的通信问题。6.1 网络接口网络接口在UNIX系统中,网络接口有两类:一类是源自BSDUNIX的Sockets(套接口),另一类是UNIXSystem
2、V的TLI(TransmissionLayerInterface)。TLI是根据工业标准“ISO传输服务定义(ISO8072)”实现的,由于SVR3只包括了流以及TLI构建模块而并没有任何的如TCP/IP之类的协议,因此TLI具有与协议无关性,关键技术是定义了一组对许多传输协议公共的服务。目前TLI的修正版XTL在UNIX系统中仍然得到广泛的使用。SocketAPI是基于各种传输协议之上的,目前已经成为网络编程的既成事实标准。基于SocketsAPI的通用性,本章只讨论SocketsAPI的应用。目前最通用的提供远程进程间通信的API是伯克利套接字(Berkeleysocket)接口。所谓的套
3、接字是一种抽象数据结构,用以创建一条在没有相关联的进程间发送、接收消息的通道(连接点)。这些进程在通信前各自建立一个Socket,并通过对Socket的读写操作实现通信功能。当使用基于套接字的连接时,服务器端进程创建一个套接字,并把它映射到一个本地地址上,然后等待(监听)客户端的请求。客户端进程也创建自己的套接字,并确定服务器端的具体位置(比如主机名,端口号等)。依靠传输连接方式的应答,客户端进程就可以开始发送和接收数据,而不用管是否接收到服务器进程的正式确认(应答)。每个套接字都有其类型和一个与之相连的进程。当应用程序创建套接字时,套接字系统调用返回句柄,即所谓套接字描述字,它和文件描述字是
4、有所区别的:当文件描述字,由open命令创建时,它被耦合到特定的文件或设备;当套接字描述字由Socket命令创建时,它并不被耦合到任何位置。当套接字用作面向连接的网络传输接口时,应用程序可用bind命令将套接字明确地耦合到一个地址。当套接字用作无连接的网络传输接口时,应用程序可以在用sendto命令发送数据报时动态地提供地址。6.1.1 套接口的类型套接口的类型UNIX提供下列四种类型的socket:数据流套接字(SOCK_STREAM),它提供双向的、面向连接的、可靠的、有序的并且不重复的无记录边界数据流。一对相连的流Socket提供几乎类似于管道的接口。流式socket针对于TCP服务应用
5、,如文件传送协议(FTP)。数据流套接字采用TCP协议,这是个有连接的协议,在数据正式传输前必须建立连接,此连接是个稳定的双向线路,可以保证提供无错误的传送管道,因为只要封包在传送过程发生错误损毁、次序错乱或送错,TCP将会察觉问题并要求重新发送数据,因此适合在需要大量的数据传输并要求完全正确的状况时使用。6.1.1 套接口的类型套接口的类型数据报套接字(SOCK_DGRAM),它也支持双向数据流,但数据以独立包形式被发送,无可靠性保证、无序、数据可能丢失或重复。数据报socket提供一个无连接服务,对应于无连接的UDP服务应用,如网络文件系统(NFS)、组播通信。数据报套接字采用UDP协议,
6、这是个无连接的协议,发送主机直接将封包送至目的主机,无需事先建立连接。因为避免了建立连接所需的高代价,采用数据报方式效率较高,但数据报方式自身不能处理数据传输过程出现的错误,因此使用数据报方式的应用程序必须自己处理这些问题。一般在比较简单的网络应用程序中使用数据报方式。6.1.1 套接口的类型套接口的类型原始套接字(SOCK_RAW),它提供对支持socket概念的基本通信协议的访问。该接口允许用户访问支持套接字抽象的底层通信协议,如IP、ICMP直接访问,常用于检验新的协议实现或访问现有服务中配置的新设备。顺序报套接字:这种类型的套接字类似数据流套接字,不同的是其传送的数据具有记录边界。6.
7、1.2 套接口支持的协议套接口支持的协议 1套接字协议簇套接字协议簇(family) (1)AF-UNIX:UNIXDomain协议,在该域中创建的套接字只能为同在一个主机的进程所用;(2)AF-INET:Internet协议,这个域里的套接字允许在不同主机上的不相关的进程间进行通讯; 2套接字协议套接字协议(Protocol) (1)TCP协议(传输控制协议):负责保证两台主机之间传输的分组到达目的地,保证分组以正确的顺序(确切的说,分组按正确的顺序重新编排)并无差错地到达目的地。当分组在两台主机间的路径上丢失时,TCP协议确保重传丢失的分组;(2)UDP协议(用户数据报协议):类似TCP协
8、议,但是它是不可靠的。UDP不对分组进行检查、重新排序和重传;6.1.2 套接口支持的协议套接口支持的协议AF_INETAF_INET6AF_LOCALAF_ROUTEAF_KEY流套接口TCPTCPY数据报套接口UDPUDPY原始套接口IPv4IPv6YY6.1.3 套接口地址结构套接口地址结构大多数套接口函数都需要一个指向套接口地址结构的指针作参数,而在实际应用中各协议的地址结构是不同的,例如IPv4是32位的,而IPv6则是128的。由于早期定义的原因,UNIX的系统函数都只支持通用的地址结构而无法区分特定协议的地址结构,因此在调用这些函数时必须将指向具体协议的套接口地址结构的指针类型转
9、换成指向通用套接口地址结构的类型。如serv是IPv4的地址格式,可以通过(structsockaddr*)&serv来转换成通用地址格式。通用套接口地址结构:(在头文件中定义)structsockaddru_charsa_len;/*地址总长度*/u_shortsa_family;/*协议族,AF_xxx*/charsa_data14;/*具体协议地址*/;其中:sa_family为套接字协议簇类型;sa_data中存储具体的协议地址,不同的协议簇有不同的地址结构,如TCP/IP协议的套接字地址结构是在文件中定义的sockaddr_in结构,这个结构定义如下:IPv4套接口地址结构:(在头文
10、件中定义)structsockaddru_charsin_len;/*32位的IPv4地址总长度*/u_shortsin_family;/*协议族,AF_INET*/u_shortsin_port;/*协议端口号*/charsin_zero8;/*保留,置0*/;要注意的是,套接口的地址结构是按网络字节顺序而不是按主机存储字节顺序来存储的(网络字节是从高到低的顺序,而主机是从低到高的顺序),因此,在Internet上传输数据时就需要进行转换,否则就会出现数据不一致。下面是几个常用的字节顺序转换函数:htonl():把32位值从主机字节序转换成网络字节序ntohl():把32位值从网络字节序转换
11、成主机字节序通用套接口地址结构IPv4套接口地址结构长度AF_INIET16位端口号sin_portsin_zero8长度AF_INIET16位端口号sin_portsa_data166.2 网络通信的系统函数网络通信的系统函数6.2.1 建立网络连接的数据结构 一个用户为了执行网络I/O操作,它首先要调用函数socket ()。这个函数陷入内核后执行sys_socket()系统调用,后者为用户创建一个名为socket的数据结构,并对其进行一些简单的初始化工作,然后返回一个小的正整数,代表这个socket结构。这个小的正整数与文件描述符功能类似,所以把它称作套接字描述符。6.2.1 建立网络连
12、接的数据结构struct socket socket_state state; unsigned long flags; struct proto_ops *ops; struct inode *inode; struct fasync_struct *fasync_list; struct file *file; struct spck *sk; wait_queue_head_t wait; short type; unsigned char passcred; unsigned char tli;其中:state描述该套接字的状态信息; flags表示连接时的一些控制信息; ops指向o
13、roto_ops 结构类型的指针; inode是指向inode 结构类型的指针; fasync_list存在异步进行处理的不同文件的指针; sk指向socket结构的指针; wait表示等待在该socket 结构上的人物列表; type表示数据包的类型。6.2.2 网络连接的建立和关闭网络连接的建立和关闭1 建立一个套接字描述符 函数格式如下: int socket(int family, int type, int protocol);socket()调用中有三个参数,第一个参数family是一个整数型的量,指定协议簇;第二个参数是type,表示套接字的类型;第三个参数protocol用来表
14、示在指定协议族中使用哪种特定协议,大多情况下,该参数被设为0,让系统自己去选择基于协议族的协议。Socket()调用成功则返回一个正整数,即套接字描述符,用以标识该套接字;如果调用失败,会返回1,并设置全局变量errno为相应的错误类型。Sockek()函数的执行流程Socket()Sys_socketcall()Sys_socket()Socket_create()Socket_alloc()Inet_creat()Sk_alloc()Sock_alloc()用来创建一个socket结构Sk_alloc()用来创建一个sock结构系统调用接口Sock_create(.)sock_alloc(
15、);inet_creat();例1:创建一个套接字对#include#include#include#include#includemain(void)intsock2,cpid,i;/*套接字对*/staticcharbufBUF_SZ;/*消息的临时缓冲区*/if(socketpair(PF_UNIX,SOCK_TREAM,0,sock)0)perror(“Generationerror”);exit(1);switch(cpid=(int)fork()case-1:perror(“Backfork”);exit(2);case0:/*子进程*/close(sock1);for(i=0;i
16、10;i+=2)sleep(1);sprintf(buf,“c:%dn”,i);write(sock0,buf,sizeof(buf);read(sock0,buf,BUF_SZ);printf(“c%s”,buf);close(sock0);break;default:/*父进程*/close(sock0);for(i=0;i10;i+=2)sleep(1);read(sock1,buf,BUF_SZ);printf(“p%s”,buf);sprintf(buf,“p:%dn”,i);write(sock1,buf,sizeof(buf);close(sock1);return0;6.2.2
17、 网络连接的建立和关闭网络连接的建立和关闭2 指定本机地址及端口 bind()intbind(intsockfd,structsockaddr*my_addr,intaddrlen);/*0成功;-1出错*/其中:参数sockfd是调用socket()返回的套接字,参数my_addr是通用地址结构指针,参数addrlen是该结构的长度,常被设置为sizeof(structsockaddr)。该函数为套接字分配一个本地协议地址,对于IP协议来说是IP地址和TCP或UDP端口号的组合。bind()调用在UNIX域中用来联系套接字和它的名字(一个文件名),在因特网域用来将本地地址和套接字绑定在一起,
18、包括IP地址和端口号。它是依据第二个参数的值的不同而不同的;套接字和本地地址的绑定采用组合的方式,如下表程序类型IP地址端口号说明服务器INADDR-ANY非0指定服务器公认端口,表示服务器愿意接收来自任何网络设备的客户机连接服务器本地IP非0指定服务器IP地址和公认端口,表示它只接收来自于这个IP地址的特定网络设备接口的客户机连接。多用于有多个网络设备接口,限制服务器的接收范围客户机INADDR-ANY非0指定客户机的连接端口,一般使用保留端口号客户机本地IP非0指定客户机的IP地址和连接端口号,表示客户机使用指定的网络设备接口和端口号进行通信客户机本地IP0指定客户机的IP地址,表示客户机
19、和指定的网络设备接口通信,系统自动为客户机选择一个未使用的端口号注:INADDR-ANY在UNIX系统中被映射为0的常量6.2.2 网络连接的建立与关闭网络连接的建立与关闭使用bind函数时,可以用下面的赋值实现自动获得本机IP地址和随机获取一个没有被占用的端口号:my_addr.sin_port=0;/*系统随机选择一个未被使用的端口号*/my_addr.sin_addr.s_addr=INADDR_ANY;/*填入本机IP地址*/6.2.2 网络连接的建立与关闭网络连接的建立与关闭3 客户启动连接connect()intconnect(intsockfd,structsockaddr*se
20、rv_addr,intaddrlen);/*返回:0成功;-1出错*/sockfd是调用socket()返回的套接字;serv_addr是包含远端服务器IP地址和端口号的通用地址结构的指针;addrlen是远端地址结构的长度。TCP客户用Connect函数建立一个与远端TCP服务器的连接,正是connect激发了TCP三次握手的连接过程。由于协议族总被包含在套接字地址结构的前两个字节中,并通过socket()调用与某个协议族相关。因此bind()和connect()无须协议作为参数。connect()用户空间核心空间sys_socketcall()sys_connect()inet_strea
21、m_coonect()tcp_v4_connect()ip_route_output()ip_route_output_key()ip_route_connect()ip_output()ip_route_optput_slow()系统调用传输层(TCP协议)网络层(IPv4或IPv6)6.2.2 网络连接的建立与关闭网络连接的建立与关闭4 监听连接listen()intlisten(intsockfd,intbacklog);/*返回:0成功;-1出错*/sockfd是调用socket()建立的套接字,它是一个socket调用成功后的返回值;backlog是指定在请求队列中允许的最大请求数,
22、一般大于5的均设为5。进入的连接请求将在队列中等待accept()调用建立与客户的连接。该函数仅被TCP服务器调用,它总是使套接口处于被动的监听模式,并为该套接口建立一个输入数据队列,将到达的服务请求保存在此队列中,直到程序处理它们。如果一个服务请求到来时,输入队列已满,该套接口将拒绝连接请求并显示出错信息。listen()调用将一个尚未连接的主动套接字转换成为一个被动套接字,使其可以接收连接请求,因为由socket()调用所创建的套接字(主动套接字)只可以用来进行主动连接,不能接收连接的请求。5 服务器接受连接accept()intaccept(intsockfd,structsockadd
23、r*addr,int*addrlen);/*返回:0成功;-1出错*/accept()调用从倾听套接字的连接队列中接收第一个连接,生成一个新的套接字来完成客户机的要求,原来的套接字继续监视网络,等待用户的连接。accept()调用的第一个参数sockfd是用来标识从哪个套接字中接收连接的,addr是一个指向客户方套接字地址结构的变量指针,该变量用来接收提出连接请求服务的客户的协议地址,指明某台主机从某个端口发出该请求;addr的确切格式由套接字创建时建立的地址族决定。addrten通常为一个指向值为sizeof(structsockaddr)的整型指针变量,指明客户方套接字地址结构的长度(字节
24、数)。accept()只用于TCP服务器,且在调用前应该先调用过listen()。当listen()侦听到有连接请求到达时,accept()调用将请求连接队列上的第一个客户的套接字地址及长度放入addr和addrlen,并与该客户在sockfd建立连接。6.2.2 网络连接的建立与关闭网络连接的建立与关闭6 6 关闭套接字关闭套接字close()close()close(intsockfd);参数sockfd是待关闭的套接字。close()是标准的关闭函数,在TCP服务中激发该套接字的连接关闭。函数功能只是对该套接字作“关闭标识”表明不可用,而连接的另一方还在试图发送排队的数据。只有当对方发现
25、通信的套接字已不可用,自己也调用close()关闭本机的套接字,才真正地结束数据的发送。6.2.3 发送数据发送数据UNIX内核为用户提供的发送数据的系统调用有5个,它们分别是:1)write():与文件系统中的write()完全一致;2)writev():与write功能相似,所不同的是writev可以在一次函数调用中写多个缓冲区(集中写)3)send():面向连接的发送数据过程(TCP);4)sendto():面向无连接的发送数据过程(UDP)5)sendmsg():直接使用msghdr结构发送数据。在功能上可以代替以上四个输出函数。1 1 发送数据的系统调用接口发送数据的系统调用接口wr
26、ite()writev()send()sendto()sendmsg()sys_write()sys_writev()sys_socketcall()sock_write()sock_write()sys_send()sys_sendmsg()sys_sendto()sock_sendmsg()inet_sendmsg()系统调用应用层BSD套接字接口INET套接字(传输层)2. 2. 从从INTEINTE协议层到协议层到IPIP层层inet_sendmsg()tcp_sendmsg()tcp_sendmsg()tcp_transmit_skb()ip_queue_xmit()网络层BSD套接
27、字接口INET套接字(传输层)2. IP2. IP层到硬件层的数据发送过程层到硬件层的数据发送过程ip_queue_xmit2()ip_output()ip_finish_output()ip_queue_xmit()net/core/dev.cIP层dev_queue_xmit()neigh_resolve_output_()ip_finish_output2()net/ipv4/ip_et/ipv4/ip_et/ipv4/ip_et/ipv4/ip_et/ipv4/ip_et/core/neighbour.c数据链路与硬件层4. 4. 硬件层的数据发送过程硬件层的数据发送过程dev_que
28、ue_xmit()ei_start_xmit()qdisc_run()ei_start_xmit()物理设备硬件层net/pkt_sched.h硬件上有队列吗?以太网卡(例如NE2000)6.2.4 接收数据接收数据UNIX内核为用户提供的接收数据的系统调用也是5个,它们分别是:1)read():与文件系统中的read()完全一致;2)readv():与read功能相似,所不同的是readv可以在一次函数调用中读多个缓冲区;3)recv():面向连接的接收数据过程(TCP);4)recvfrom():面向无连接的接收数据过程(UDP)5)recvmsg():直接使用msghdr结构来接收数据。
29、在功能上可以代替以上四个输出函数。1 1 接收数据的系统调用接口接收数据的系统调用接口read()readv()recv()recvfrom()recvmsg()sys_read()sys_readv()sys_socketcall()sock_read()sock_readv()sys_recv()sys_recvmsg()sys_recvfrom()sock_recvmsg()inet_recvmsg()系统调用应用层BSD套接字接口INET套接字(传输层)2. 硬件层接收数据分析netif_rx()ei_recieve()ei_interrupt()ip_rev()net/core/de
30、v.cdrivers/net/8390.cdrivers/net/8309.c硬件层net_rx_action()IP层物理设备NE网卡向队列写入数据Backing接收数据队列从队列中读出数据2. 硬件层接收数据分析3. 从IP层接收数据tcp_v4_rcv()ip_local_deliver_finish()ip_local_deliver()tcp_rev_established()INET层net_rx_action()ip_rev()ip_rcv_finish()net/ipv4/tcp_et/ipv4/ip_et/ipv4/ip_et/ipv4/input.c硬件层tcp_v4_do
31、_rcv()receive_queue队列IP层4. 4. 从从INTEINTE层接收数据层接收数据6.3 套接字编程方法套接字编程方法1面向连接的数据流套接字时序步骤流套接字的服务器进程和客户机进程在进行通信前必须建立一条连接,其中,初始化连接的是客户端进程,接收连接的进程是服务器端的进程。建立连接和通信的主要步骤如下:(1)服务进程首先调用Socket()创建一个流套接字;(2)服务进程调用bind()公开服务器地址,将服务器地址与套接字绑定在一起;(3)服务进程调用listen()将套接字转换成倾听套接字,此时该套接字可以接收来自客户机的请求;(4)通过accept()阻塞服务进程,此时
32、该服务器进入一个无限循环,等待客户进程建立连接;(5)客户进程也通过Socket()创建一个流套接字,然后调用connect()与服务进程建立连接;(6)当客户进程的连接请求到达服务器后,服务进程进程被唤醒,生成一个新的套接字,服务进程用这个新的套接字按预先定义的协议调用read()和write()进行通信,处理客户进程的要求;而服务进程最早生成的套接字则继续用于监听网络上的服务请求;(7)处理完成后,服务进程和客户进程调用close()关闭这个连接和套接字;服务器Socket()、bind()、listen()accept()read()write()read()close()阻塞,等待客户
33、机连接请求coonect()write()read()socket()close()客户建立连接(TCP三路握手)发送数据请求接收数据通信结束、关闭连接面向连接的数据流套接字通信模型面向连接的数据流套接字通信模型2 面向连接的数据流套接字的典型编程方法(1)服务器一方main(void)if(创建一个流套接字返回值0)出错提示;退出;if(命名套接字返回值0)出错提示;退出;if(监听连接请求返回值0)出错提示;退出;for(;)新的套接描述符=取得第一个连接请求返回代码;if(新的套接描述符0)出错提示;退出;接收数据信息;处理请求;将应答发送给客户机;关闭套接字;(2)客户一方main(v
34、oid)if(创建一个流套接字返回值0)出错提示;退出;if(连接服务器返回值0)出错提示;退出;for(;)发送数据信息;接收服务器方应答;关闭套接字;6.4 无连接的数据流套接字的编程方法 1 无无连接的数据流套接字时序步骤连接的数据流套接字时序步骤数据报套接字的服务器进程和客户机进程在进行通信前不用建立连接。通信的主要步骤如下:(1)服务进程首先调用Socket()创建一个数据报套接字;(2)服务进程调用bind()将服务器地址绑定在在这个套接字上;(3)通过recvfrom()阻塞服务进程,等待客户进程发来的请求;(4)客户机首先调用Socket()创建一个数据报套接字;(5)客户机进
35、程调用bind()将客户机地址绑定在在此套接字上;(6)调用sendto(),客户机进程向服务进程发出请求;(7)服务进程接到客户机数据报后被唤醒,执行完客户机请求后调用sendto()将处理结果返回给客户机;(8)客户机调用recvfrom()接收服务进程返回的请求处理结果;(9)服务进程和客户进程调用close()撤消套接字;socket()服务bind()recvfrom()进程请求sendto()socket()sendto()read()close()客户recvfrom()数据(请求)数据(回答)阻塞直至收到来自客户的数据报2 非连接的数据流套接字的通信模型3 非连接的数据流套接字
36、的典型编程方法(1)服务器一方main(void)if(创建一个数据报套接字返回值0)出错提示;退出;if(命名套接字返回值0)出错提示;退出;for(;)接收客户机数据报(请求);处理请求;将数据(结果)发送给客户机;关闭套接字;3 非连接的数据流套接字的典型编程方法(1)客户端一方main(void)if(创建一个数据报套接字返回值0)出错提示;退出;if(命名该套接字返回值0)出错提示;退出;for(;)发送数据报(请求)给服务器;接收服务器应答;关闭套接字;6.5 6.5 基于客户基于客户/ /服务器模式的网络编程服务器模式的网络编程1 客户/服务器的工作流程(1)必须使用标准函数库实
37、现系统调用,以陷入内核获得完成用户任务所需要的系统软、硬件资源;(2)系统调用都被内核入口点system_call函数截获,该函数根据所传递的参数确定应该执行哪些系统调用,并通过检查系统调用表,以确定相应的服务例程。最后把控制权转给该服务例程;(3)该服务过程立即和相关的内核代码模块建立联系。这些模块可能进一步需要和其他内核模块或者底层硬件通讯;(4)当系统调用结束后,结果按照相同的路径反方向返回。核心把控制交给用户程序;(5)如果该实例有某些错误的操作,如用户堆栈溢出,处理器将引发一个异常通知内核,有内核执行相应的处理程序来处理异常事件。客户端服务器端1.解析域名生成服务器请求2.计算并显示
38、服务内容和图像BuffetalloctedSocketbuffer客户程序系统调用 Connect()recv()send()fputs()TCP/IP协议内核NIC缓冲网卡1.解析域名生成服务器请求2.计算并显示服务内容和图像BuffetalloctedSocketbufferTCP/IP协议NIC缓冲accept()recv()send()open()read()文件系统RAMorDisk服务器程序内核网卡HTTPTCPIP网络协议Internet操作系统支持Web服务器的工作流程1 客户/服务器的工作流程在客户端,如果运行的是Web测览器,当向Web浏览器的“地址”窗口处输入http:/
39、.或者“WWW.”等形式的网址并回车,就是由Web浏览器向web服务器发出的一次客户请求。该请求经过解析后,通过系统调用由用户态转为核心态执行。在核心态操作系统中的TCP/IP协议代码和网卡驱动程序控制网卡把请求发送到相应的网络上,等待Web服务器响应。当服务响应返回时,由网卡接收,并通过内核传送给客户程序。在服务器端,内核通过网卡从网络上接收Web请求,并通过系统调用传递给Web服务器。Web服务器根据此服务请求执行相应的服务过程,并由内核把结果放到网络上传送给客户。Web浏览器和Web服务器使用URL和URLConnection进行网络通信,这是一种较高层次的网络通信,为的是访问Inter
40、net上的资源。通常,用户也常常需要编写一个由操作系统提供的接口客户/服务器应用程序,由套接口实现的客户服务器应用程序是低级别的网络通信。因此,从程序员的观点来看,操作系统所提供的系统调用定义了应用程序和协议栈之间的接口。应用程序无论使用哪种通信协议进行网络通信,都必须申请同操作系统交互才能得到服务。#include#include#include#include#include#include#include#includeint main(int argc,char *argv) int sockfd,numb, port = 8000; struct sockaddr_in s; str
41、uct hostent *host; char buf100; if(argc!=2) fprintf(stderr,usage:%s hostnamen,argv0); exit(1); if(!(host=gethostbyname(argv1) perror(error in resolving hostname); exit(1); sockfd=socket(AF_INET,SOCK_STREAM,0); if(sockfdh_addr)-s_addr;s.sin_port=htons(port);if(connect(sockfd,(structsockaddr*)&s,sizeo
42、f(s)!=0)perror(connect);exit(1);printf(getconnected!n);getchar();strcpy(buf,Givemethefirstfileofthepresidentsoffice);if(send(sockfd,buf,strlen(buf),0)0)perror(send);exit(1);numb=recv(sockfd,buf,100,0);if(numb0)perror(recv);exit(1);bufnumb=0;printf(Received=%sn,buf);close(sockfd);return0;在这个例子中,客户程序向
43、服务器发出请求,建立与服务器的TCP连接,并发送服务请求字符串“Givemethefirstfileofpresidentsoffic”。在client1.c客户程序中,先调用socket()函数创建一个套接口,然后调用connect()函数数建立与主机的连接。当connect()函数返回时,再通过调用send()函数向服务器发出请求或数据。接下来执行recv()函数等待接收服务器发回的响应。下面的程序清单给出的是服务器程序server1.c。服务器程序创建一个永久套接口来侦听服务请求。当接收到客户的服务请求时,它将创建一个临时套接口,传回一个字符串,然后将关闭临时套接口,循环等待下一个客户请
44、求的到来。为了不影响前台其他程序的运行,可以把服务器程序写成一个守护进程(daemon),守护进程启动后一直运行在后台,监听客户的请求,从不退出,除非显式地将它关闭。#include#include#include#include#include#include#include#include#include#includeintport=8000;voidinit_daemon(void)pid_tpid;inti;if(pid=fork()=-1)/*生成第1个子进程*/exit(1);/*fork失败退出*/if(pid0)exit(0);/*父进程退出,使shell成为前台进程*/se
45、tsid();/*第1子进程成为新会话和新进程组的领头进程的同时也失去控制终端*/*第1子进程执行下面的操作*/for(i=0;iNOFILE;+i)close(i);/*关闭已打开的文件描述符*/chdir(/rundir);/*改变当前运行的目录*/umask(0);/*改变文件创建掩码*/return;intmain(intac,char*av)intsockfd,tsockfd,addr_size;charbuf100;structsockaddr_ins,p;chartemp_buf256;init_daemon();sockfd=socket(AF_INET,SOCK_STREAM
46、,0);if(sockfd=-1)perror(socketbuildingfailure);exit(1);bzero(&s,sizeof(s);s.sin_family=AF_INET;s.sin_addr.s_addr=INADDR_ANY;s.sin_port=htons(port);if(bind(sockfd,(structsockaddr*)&s,sizeof(s)=-1)perror(bind);exit(1);if(listen(sockfd,10)=-1)perror(listen);exit(1);for(;) tsockfd=accept(sockfd,(structs
47、ockaddr*)&p,&addr_size); if(tsockfd0)printf(ft:%dn,errno);perror(accept);exit(1); if(recv(tsockfd,buf,100,0)0)perror(recv);exit(1); strcpy(temp_buf,Hereisthefirstfileofthepresidentsoffice.); if(send(tsockfd,temp_buf,strlen(temp_buf),0)0)perror(send);exit(1); close(tsockfd);printf(send:%s,temp_buf);在
48、这个服务器程序的例子中,首先执行socket()系统调用,让内核创建一个永久的接口,以监听服务器请求。然后调用bind()函数将刚创建的套接口与众所周知的端口号8000连在一起。接着调用listen()函数将永久套接口变成一个被动套接口,开始监听来自客户机的连接请求。当函数返回时,程序将进入一个无限循环,并阻塞在accept()系统调用上,等待连接请求的接入。一旦有接入请求,accePt()马上返回一个临时套接口供服务器读取用户的请求数据。然后将用户需要的数据返回给客户。读取用户数据的系统调用是recv()函数,发送数据的系统调用是send()函数。网络上数据的传输方向是双向的。一个流向是把数
49、据从图l6a的用户层传输到网卡,另一个流向是把数据从网卡传输到用户层。无论流向哪一方,中间都要穿过内核层。下面给出编译并运行这两个程序的操作过程:$gcc-ociient1clientl.c$gcc-oserver1server1.c操作如下:$./server1$./client1localhostgetconnected!(回车)Received=Hereisthefirstfileofthepresidentsoffice3客户/服务器程序的编程实现流程实现流程下图是基本的面向连接的client/sever系统调用流程。在客户端编程的基本步骤是:创建套接字;利用创建的套接字向服务器发出连
50、接请求,启动TCP的三次握手;如果连接成功,双方互操作开始。服务器端通信软件的步骤是:创建套接字;在建立的套接字中绑定本机IP及一个周知的端口号;在该套接字进行监听;如收到连接请求,则启动与客户的连接;如果连接成功,双方互操作开始。3 TCP3 TCP连接的建立和终止过程连接的建立和终止过程TCP传输控制协议提供客户与服务器的连接。一个TCP客户与一个TCP服务器可以建立一个可靠的连接,并通过这个连接通路与服务器交换数据,通信结束后终止连接。 (1) 建立TCP连接的过程建立一个TCP连接,必须首先启动服务器。服务器程序通过调用函数socket()、bind()和listen(),事先做好接收
51、客户的连接请求的准备。这个过程称为被动打开。稍后启动客户机程序,它调用函数socket()创建一个用于连接的套接口,然后调用函数connect()执行连接请求,从而引发了客户机与服务器之间一个TCP连接的建立过程,调用函数connect()的过程称为主动打开。下图给出了建立TCP连接的示意图。客户机socket()accept()(阻塞)Accept()(返回)socket()bind()listen()connect()(阻塞)Connect()(返回)read()(阻塞)服务器SYNBSYNDackB+1ackD+1建立TCP连接的三次握手3 TCP3 TCP连接的建立和终止过程连接的建立
52、和终止过程建立一个TCP连接需要三路握手信号:(l)客户机通过调用connect()执行,首先向服务器发送一个SYN(同步序列号标志)分节。这里所说的分节是TCP传递给IP的数据单元。一个SYN分节一般不携带数据,它只含有一个IP头部和一个TCP头部,还含有一些TCP的选项。这些选项告诉服务器在本次连接中发送数据的初始序列号。图1中给出的客户机初始序列号是B。(2)服务器在收到客户发来的SYN分节以后,必须给客户机一个回答信号,确认客户的SYN分节。与此同时,服务器也要向客户机发送一个SYN分节,通知客户机服务器将在这次连接中发送数据的初始序列号D。(3)在收到服务器发来的SYN分节和回答信号
53、后,客户也必须回答确认服务器的SYN分节。由于SYN只占一个字节的序列号空间,所以客户、服务器通信双方的ACK回答的确认序列号都是由所接收到对方的序列号加1得到。2 TCP2 TCP连接的建立和终止过程连接的建立和终止过程(2) 终止TCP连接的过程用户可以显式地调用close()函数来关闭一个TCP连接。关闭一个TCP连接需要4个分节。关闭连接也有主动关闭和被动关闭之分。先调用close()函数的一端称为主动关闭。当close执行时,发送一个FIN(完成标志)分节,通知对方数据发送完毕。FIN是finishflap的缩写,它也是TCP分节之一。接收FIN分节的另一端称为被动关闭。FIN分节作
54、为文件结束符由另一端的read()函数来接收。因为接收到FIN分节就意味着另一端的进程在此连接上再也不会接收到数据了,所以read()返回0。但这个FIN分节是由被动关闭一端的TCP来确认。read()函数返回后,应用进程也将调用close()函数来关闭相应的套接口。close()函数执行时,也会向对方发送一个FIN分节,并由主动关闭一端的TCP对其进行确认。图2给出关闭套接口时分组的交换过程,总共需要交换4个分组。客户机close()close()read()(阻塞)服务器FINXackX-1TCP关闭连接时的分组交换过程FINZackZ-13 TCP3 TCP连接的建立和终止过程连接的建立
55、和终止过程从TCP连接的建立和终止过程可以看出,如果在TCP协议的一次通信中,只发送一个单一分节的请求和接收一个单一分节的应答,则需要7个分节的额外开销。如果使用UDP,只有“请求”和“应答”两个分组需要交换,但是UDP不提供可靠的数据包传送,它把传输层的许多处理推给UDP的应用程序。尽管如此,还是有许多应用程序使用UDP协议,因为它没有TCP建立连接和终止连接的额外开销。 4 编制编制TCP客户程序客户程序客户机应用程序有几个特点:首先,客户程序一般不要求处理并发性,因为在同一时刻一个客户程序只能与一个服务器进行交互。第二,客户程序的执行不需要特定的保护,它和普通的应用程序一样执行,不需要特
56、权。鉴于以上两点,可知设计和编写客户程序比较容易。(1) TCP(1) TCP客户程序设计方法客户程序设计方法设计一个TCP客户程序步骤如下:1)确定与之通信的服务器的IP地址和端口号。2)调用socket()函数创建一个TCP套接口。3)为这次连接分配一个在本地机器中的、未使用的协议端口号。4)调用connect()函数建立与服务器的连接。5)用输人和输出函数进行通信。6)调用close()关闭连接。 4 编制编制TCP客户程序客户程序(2) TCP(2) TCP客户程序的实现客户程序的实现以下给出了一个客户程序的例子clientc,以便了解编制客户程序应该具有的基本语句和算法。在这个程序中
57、,客户建立与服务器的TCP连接并向服务器发送一条消息询问当前时间,然后调用read()函数等待读取服务器送回的当前时间和日期。#include #include #include #include #include char * host_name = 127.0.0.1; /* local host */int port = 8000;void main(int argc, char *argv) char buf8192, message256; int sockfd, n; struct sockaddr_in pin; struct hostent *shost_name; if (s
58、host_name = gethostbyname(host_name) = 0) /*域名转换成主机地址*/ perror(Error resolving local hostn); exit(1); /* 首先,客户机必须知道服务器主机首先,客户机必须知道服务器主机IP的地址,如果使用同一台计算机作为的地址,如果使用同一台计算机作为服务器运行,则本地计算机的标准服务器运行,则本地计算机的标准IP地址是地址是127.0.0.1,所以程序中获得主,所以程序中获得主机信息的语句是:机信息的语句是: */ bzero(&pin, sizeof(pin); pin.sin_family = AF_I
59、NET; pin.sin_addr.s_addr = htonl(INADDR_ANY); pin.sin_addr.s_addr = (struct in_addr *)(shost_name-h_addr)-s_addr; pin.sin_port = htons(port); if (sockfd = socket(AF_INET, SOCK_STREAM, 0) = -1) /* 创建一个与服务器进行连接的创建一个与服务器进行连接的 TCP套接口套接口*/ perror(Error opening socketn); exit(1); if(connect(sockfd,(void*)
60、&pin,sizeof(pin)=-1)/*建立与服务器的连接*/perror(Errorconnectingtosocketn);exit(1);sprintf(message,Whatisthetimeanddaten);printf(Sendingmessage%stoserver.n,message);if(write(sockfd,message,strlen(message)=-1)/*向服务器发送数据请求*/perror(Errorinwriten);exit(1);printf(.sentmessage.waitforresponse.n);while(n=read(sockf
61、d,buf,8192)0)/*接收来自服务器的字符,放在缓冲区buf*/bufn=0;if(fputs(buf,stdout)=EOF)perror(Errorfputsn);exit(1);if(n0)/*显示来自服务器的字符信息*/printf(nResponsefromserver:nn%sn,buf);exit(1);close(sockfd);/*通信目的完成,关闭与服务器的连接*/ 5 编制编制TCP服务器程序服务器程序客户机程序是一个单个用户运行来访问远程服务器的程序,它要求只经过很短的延时、最好没有延时就能得到答复。当从提供服务的主机的角度去考虑问题时,情况将变得非常复杂。因为
62、分布在各地的用户可能在同一时刻访问同一台给定的服务器,每个用户都期望得到无时延的响应。为了尽最大可能满足所有用户的要求,提供应用服务的主机必须能够快速响应并处理多个请求,因此,服务器必须使用并发处理。由于用户对并发的需要使得服务器程序的设计、编程和维护都趋向复杂化,需要使用新的算法和新的编程技术。衡量一个服务器程序的标准同样是可靠性。好用和高吞吐率。下面先介绍一种面向连接的、循环串行处理客户请求的服务器算法和编程技术。它是服务器编程技术中最简单的,因此称它为一个简单的TCP服务器程序。 编制编制TCP服务器程序服务器程序(2)(2)一个简单一个简单TCPTCP服务器的编程方法服务器的编程方法
63、所谓简单的TCP服务器程序是指一个单进程一次只处理一个客户请求的服务器程序。设计一个简单TCP服务器程序的步骤如下:l)调用socket()创建一个套接口。2)调用bind()把套接口绑定到自己所提供服务的知名端口上。3)调用listen()将套接口设置为被动模式,使之准备接受外来的连接请求。4)调用accept()接受下一个连接请求,并获得连接的一个新套接口。5)读取来自客户机的请求,并处理该客户请求,然后向客户发回响应数据。6)当与该客户完成通信时,关闭此次连接,并返回步骤等待接受下一个新的连接请求。 编制编制TCP服务器程序服务器程序 (3 3)一个简单)一个简单TCPTCP服务器的实现
64、服务器的实现 以下给出的程序server.c是为client.c客户程序提供服务的服务器程序。程序server.c在接收到客户询问当前时间的请求之后,便处理这个请求,并将结果发回给客户。这个程序允许接纳20个服务请求的输入队列。它必须和client.c程序一道工作,先启动server.c,稍后启动client.c程序。#include#include#include#include#include#includeintport=8000; void main() struct sockaddr_in serveraddr, clientaddr; /*服务器和客户端地址信息服务器和客户端地址信
65、息*/ int sockfd,temp_sockfd, clientaddr_size; char buf16384; time_t ticks; sockfd = socket(AF_INET, SOCK_STREAM, 0); /*创建创建IPv4的数据流的数据流socket*/ if (sockfd = -1) perror(call to socket); exit(1); bzero(&serveraddr, sizeof(serveraddr); /*清空地址结构,以便下面填入套接字参数清空地址结构,以便下面填入套接字参数*/ serveraddr.sin_family = AF_
66、INET; /*指定服务器使用协议为指定服务器使用协议为IPv4*/serveraddr.sin_addr.s_addr = INADDR_ANY; /*由系统自获取本机地址由系统自获取本机地址*/ serveraddr.sin_port = htons(port); /*指定服务器用于监听的端口指定服务器用于监听的端口*/ if (bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr) = -1) /*把本机协议地址、端口号绑定于刚创建的套接字把本机协议地址、端口号绑定于刚创建的套接字sockfd上上*/ perror(
67、call to bind); exit(1); if (listen(sockfd, 20) = -1) /*套接字套接字sockfd用于监听客户的请求用于监听客户的请求*/ perror(call to listen); exit(1); printf(Accepting connections .n); while(1)/*服务器无限循环等待客户连接请求*/temp_sockfd=accept(sockfd,(structsockaddr*)&clientaddr,&clientaddr_size);iftemp_sockfd=-1)perror(call to accept); exit
68、(1); if(read(temp_sockfd,buf,16384)=-1)/*由子进程提供服务*/perror(calltoread);exit(1);printf(receivedfromclient:%sn,buf);ticks=time(NULL);sprintf(buf,%srn,ctime(&ticks);if(write(temp_sockfd,buf,strlen(buf)=-1)perror(calltowrite);exit(1);close(temp_sockfd); 程序程序的分析 程序一开始先调用程序一开始先调用socket()创建一个套接口,然后调用创建一个套接口
69、,然后调用bind()函数将知名端口函数将知名端口 13捆绑到这个套接回。程序中指定捆绑到这个套接回。程序中指定IP地址为地址为INADDRANY,它意味着允许服务器在任意端口上接收来自客,它意味着允许服务器在任意端口上接收来自客户的连接请求。户的连接请求。 通过调用通过调用listen()函数将此套接口转变成一个监听套接口,以函数将此套接口转变成一个监听套接口,以便使内核开始监听连接到便使内核开始监听连接到13号端口上的客户连接请求。号端口上的客户连接请求。socket()、bind()和和listen()是所有是所有TCP服务器监听客户连接所必须的服务器监听客户连接所必须的“三步曲三步曲”
70、。 接着接着server.c进入一个无限循环等待来自客户的连接请求。一进入一个无限循环等待来自客户的连接请求。一般情况下,服务器进程在调用般情况下,服务器进程在调用accept()函数之后处于阻塞状态。函数之后处于阻塞状态。等待着客户连接请求的到达。当等待着客户连接请求的到达。当TCP连接的三路握手信号结束时,连接的三路握手信号结束时,accept()返回,它的返回值是内核为刚连接到服务器的客户创建的一个返回,它的返回值是内核为刚连接到服务器的客户创建的一个新的、已连接的套接口描述符特新的、已连接的套接口描述符特temp_sockfd。随后,函数。随后,函数read()使用这个套接口描述符接收
71、来自客户端的服务请求;并打印这使用这个套接口描述符接收来自客户端的服务请求;并打印这个信息。接着执行请求服务,调用个信息。接着执行请求服务,调用time()函数获取当前时间和日函数获取当前时间和日期,并用函数期,并用函数ctime()将这个时间(秒数)转变成人们习惯的阅读将这个时间(秒数)转变成人们习惯的阅读格式。最后调用格式。最后调用write()函数,将时间数据发回客户端。函数,将时间数据发回客户端。 程序程序的分析 程序一开始先调用程序一开始先调用socket()创建一个套接口,然后调用创建一个套接口,然后调用bind()函数将知名端口函数将知名端口 13捆绑到这个套接回。程序中指定捆绑
72、到这个套接回。程序中指定IP地址为地址为INADDRANY,它意味着允许服务器在任意端口上接收来自客,它意味着允许服务器在任意端口上接收来自客户的连接请求。户的连接请求。 通过调用通过调用listen()函数将此套接口转变成一个监听套接口,以函数将此套接口转变成一个监听套接口,以便使内核开始监听连接到便使内核开始监听连接到13号端口上的客户连接请求。号端口上的客户连接请求。socket()、bind()和和listen()是所有是所有TCP服务器监听客户连接所必须的服务器监听客户连接所必须的“三步曲三步曲”。 接着接着server.c进入一个无限循环等待来自客户的连接请求。一进入一个无限循环等
73、待来自客户的连接请求。一般情况下,服务器进程在调用般情况下,服务器进程在调用accept()函数之后处于阻塞状态。函数之后处于阻塞状态。等待着客户连接请求的到达。当等待着客户连接请求的到达。当TCP连接的三路握手信号结束时,连接的三路握手信号结束时,accept()返回,它的返回值是内核为刚连接到服务器的客户创建的一个返回,它的返回值是内核为刚连接到服务器的客户创建的一个新的、已连接的套接口描述符特新的、已连接的套接口描述符特temp_sockfd。随后,函数。随后,函数read()使用这个套接口描述符接收来自客户端的服务请求;并打印这使用这个套接口描述符接收来自客户端的服务请求;并打印这个信
74、息。接着执行请求服务,调用个信息。接着执行请求服务,调用time()函数获取当前时间和日函数获取当前时间和日期,并用函数期,并用函数ctime()将这个时间(秒数)转变成人们习惯的阅读将这个时间(秒数)转变成人们习惯的阅读格式。最后调用格式。最后调用write()函数,将时间数据发回客户端。函数,将时间数据发回客户端。6 并发服务器的设计并发服务器的设计server.c程序称为面向连接的循环服务器或称为面向连接的迭代服务器。它一次只能处理一个客户,当有多个客户请求同时到达时,要用listen()函数中第二个参数,它是内核允许接入的最大排队数目。在这个队列中,内核每次返回一个给accept()函
75、数。在这个例子中服务器的服务响应速度是非常快的,因为它在服务期间只执行了两个库函数。如果服务器服务的项目是费时的操作,其他排队等待的客户势必要等待很长时间,因此就必须重新寻求新的方式,以便能够同时为到达的多个客户请求进行服务。这种能够同时为多个客户进行服务的程序称为并发服务器。并发服务器能同时处理多个客户请求,下面给出的实例是在并发服务器编程中使用fork()函数为每个到达的客户派生一个子进程,由这个子进程处理客户的请求。除此之外,还有许多其他编写并发服务器的技术,比如使用线程代替fork()调用,或在服务器运行前预先执行fork(),创建一定数量的子进程,等等。6 并发服务器的设计并发服务器
76、的设计并发实现:可以通过进程并发或线程并发实现。并发进程是当服务器收到客户请求并accept()后,调用fork派生一个子进程来为该客户程序服务,自己则回到等待状态,准备接收下一个连接请求,子进程则在服务完成后退出。并发进程为每个客户均fork一个子进程,即每客户单进程服务,子进程可以即时派生,也可以预先派生一定的数量以备系统调用。并发线程是指当有客户连接时由主线程创建子线程为客户提供服务,这种方法的执行效率更高。下面只对并发进程设计作详细介绍。(2)并发进程实现并发进程实现在介绍编写并发服务程序前,首先来了解一下服务器并发进程是如何实现的,也就是理解UNIX的fork函数。fork()是UN
77、IX中派生子进程的唯一方法,在调用中系统将从父进程虚空间到子进程虚空间的拷贝,两个进程的代码段和用户数据段是完全相同的,并且两个进程的系统数据段也几乎相同。但是它们有各自的数据结构且进程标志符是不同的。调用格式为:pid_tfork(pid);/*返回:在子进程中为0;在父进程中为子进程的ID;-1出错*/fork函数调用一次却返回两个不同的值,是因为fork()以后的某个时刻,子进程创建后当前运行的仍然是父进程。因此,子进程的上下文被保存,并进入到就绪队列中等待调度。因此,子进程的上下文被保存,并进入到就绪队列中等待调度。当父进程运行结束后将会返回子进程的标识符,而父进程也将从核心态转化成用
78、户态。当子进程得到调度并投入运行后,由于子进程与父进程的代码段相同,子进程同样也调用fork(),只不过不真正创建子进程,只是返回一个0值,然后子进程在自己的虚空间中运行。我们可以通过返回值是否为0判断当前进程是父进程还是子进程。值得注意的是,父进程在fork之前打开的所有描述字在fork后均与子进程共享,并发服务程序设计正是利用这一特性实现的。当服务器accept连接,并调用fork后,已经连接的套接口就在父进程与子进程间得到共享,此时套接口描述符中访问计数项的记录为2,表示该套接口被两个进程访问。此后父进程关闭,访问计数减为1,因此,子进程还在继续访问该套接口并为该口的连接客户提供服务。只
79、有再次close,计数变为0该套接口的连接才真正被关闭。#include#include#include#include#include#includeintport=8000;voidmain()structsockaddr_inserveraddr,clientaddr;/*服务器和客户端地址信息*/intsockfd,temp_sockfd,clientaddr_size;charbuf16384;time_tticks;pid_tpid;sockfd=socket(AF_INET,SOCK_STREAM,0);/*创建IPv4的数据流socket*/if(sockfd=-1)perro
80、r(calltosocket);exit(1); bzero(&serveraddr, sizeof(serveraddr); /*清空地址结构,以便下面填入套接字参数清空地址结构,以便下面填入套接字参数*/ serveraddr.sin_family = AF_INET; /*指定服务器使用协议为指定服务器使用协议为IPv4*/ serveraddr.sin_addr.s_addr = INADDR_ANY; /*由系统自获取本机地址由系统自获取本机地址*/ serveraddr.sin_port = htons(port); /*指定服务器用于监听的端口指定服务器用于监听的端口*/ if
81、(bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr) = -1) /*把本机协议地址、端口号绑定于刚创建的套接字把本机协议地址、端口号绑定于刚创建的套接字sockfd上上*/ perror(call to bind); exit(1); if (listen(sockfd, 20) = -1) /*套接字套接字sockfd用于监听客户的请求用于监听客户的请求*/ perror(call to listen); exit(1); printf(Accepting connections .n); while(1)/*服务
82、器无限循环等待客户连接请求*/temp_sockfd=accept(sockfd,(structsockaddr*)&clientaddr,&clientaddr_size);if(pid=fork()=0)/*父进程派生一个子进程*/close(sockfd);/*关闭父进程:监听套接字*/if(read(temp_sockfd,buf,16384)=-1)/*由子进程提供服务*/perror(calltoread);exit(1);printf(receivedfromclient:%sn,buf);ticks=time(NULL);sprintf(buf,%srn,ctime(&tick
83、s);if(write(temp_sockfd,buf,strlen(buf)=-1)perror(calltowrite);exit(1);close(temp_sockfd);exit(0);close(temp_sockfd);编译并运行client.c和serverf.c,显示执行结果。$gcc-oclientclient.c$gcc-oserverfserverf.c先运行serverf服务器程序。屏幕显示结果如下:$./serverfAccoptingconnectionsReceivedfromclient:Whatisthetimeanddate打开第二个窗口,运行client
84、客户程序。屏幕显示结果下:$./clientSendingmessageWhatisthetimeanddatatoserver.sentmessagewaitforresponseMonDec911:20:4220026.6 UDP套接字编成的基本方法套接字编成的基本方法用户数据报协议UDP是在传输性能上比TCP更低级的协议,它和TCP协议存在着本质的差异。因为UDP不面向连接所以它不提供由保证的信息传输,更不提供有保证的传输错误提示。除此之外,UDP也不能保证消息以被发送时的顺序到达。而TCP是面向连接的,因此它提供可靠的宇节流等许多保证。但是在某些情况下,使用TCP协议不一定是最佳选择,
85、因为TCP的连接和终止有交换7个分组的额外开销。如果使用UDP,那么只有两个分组需要交换:请求和应答。这样在发送的数据块不是很大的情况下,有些应用程序可以使用UDP协议。例如:可以将传输的数据放在一个物理UDP数据报中,并允许一些传输数据丢失。只要丢失的数据不损坏所传输的信息的完整性即可,因此这部分丢失的数据就不需要重发。6.6 UDP套接字编成的基本方法套接字编成的基本方法DNS域名系统、NFS网络文件系统和SNMP(简单网络管理协议)等都使用的是UDP协议。通常,在局域网上UDP数据包很少丢失。在选择UDP还是是TCP传输数据时,还要考虑另外一个因素,即通信应用程序是否需要广播或组播通信。
86、面向连接的TCP协议之提供点到点通信,而不能提供广播或组播通信;所以当然需要广播或组播通信服务时,需要使用UDP的无连接服务。下图给出了使用UDP编写客户服务器程序所需要的基本函数。在客户端不需要调用函数connect()与服务器建立连接,它直接调用sendto()函数向服务器发送数据就可以了。同样在服务器端也不需要监听套接口等待用户的连接请求,它直接调用recvfrom()函数等待客户数据的到达。socket()sendto()recvfrom()close()sendto()处理客户请求recvfrom()socket()bind()阻塞,等待收到客户的数据应答请求UDP客户UDP服务器U
87、DP客户/服务器的程序流程示意图1UDP程序使用的套接口函数从上图中可以观察到一般UDP套接口无论是客户端还是服务器端都不能使用connect()函数,那样只能与一个特定的远程计算机和端点通信,服务器就不能使用一个套接口接收来自任意客户机的数据报。所以UDP服务器使用的套接口只能是一个非连接的,客户和服务器双方直接使用sendto()向对方发送请求和数据。它的函数原形如下:#includessize_tsendto(intsockfd,constvoid*buf,size_tlen,intflags,conststructsockaddr*to,socklen_taddrlen);第一个参数s
88、ockfd是由socket()函数返回的套接口描述符,第二个参数buf是指向存放发送数据的缓冲区指针;第三个参数len指明缓冲区中的字节数;第四个参数flags表明排错或者控制选项;第五个参数to是一个指向sockaddr_in结构的指针,结构中含有将报文要发往的IP地址和端口号;最后一个参数addrlen指明这个地址结构的大小。1UDP程序使用的套接口函数在UDP套接口编程中使用的另一个函数是recvfrom(),它可以使客户服务器双方接收对方的请求和数据。它的函数原形如下:#includessize_trecvfrom(intsockfd,void*buf,size_tlen,intfla
89、gs,structsockaddr*from,socklen_t*addrlen);函数前四个参数的意义等同于sendto()函数的前四个参数:套接口描述符、指向读人缓冲区的指针和写人的字节数以及排错或控制选项。第五个参数from所指向的套接口地址结构中,装有数据报发送者的协议地址;最后一个参数addrlen所指向的地址装有此套接口地址结构中的字节数。这里还需要进一步说明的是recvfrom(),它的最后两个参数是函数返回时装有发送数据报的协议地址和它的长度。而sendto()函数的最后两个参数是将数据报发往目的地的协议地址和地址结构的长度。1UDP程序使用的套接口函数以上两个函数调用成功时都
90、返回所读写的字节数,出错时返回-l。在UDP情况下,一个长度为0的数据报包含一个IP头部(IPv4的头部长20个字节,而IPv6的头部长ap个字节)、8字节的UDP头部和没有数据的IP数据报。所以当recvfrom()函数返回0时表示对方已停止发送,等同于TCP的“已关闭连接”。在调用recvfrom()函数时,如果将from设置为空指针,则最后一个参数addrlen也必须是NULL,这样内核就知道用户不关心发送方的协议地址。2发送UDP数据报的编程实例以下给出了一个发送程序send.c,它向接收进程发送10个文本消息。#include#include#include#include#incl
91、udeintport=9889;voidmain()intsockfd,n;intcircle=0;intprocess;charsendbuf128;/forsendingmessagescharrecvbuf128;/forreceivemessagesstructsockaddr_inaddr;sockfd=socket(AF_INET,SOCK_DGRAM,0);if(sockfd=-1)perror(Openingsocket);exit(1);memset(&addr,0,sizeof(addr);addr.sin_family=AF_INET;addr.sin_addr.s_ad
92、dr=inet_addr(127.0.0.1);addr.sin_port=htons(port);process=1;dosprintf(sendbuf,datapacketwithID%dn,circle);if(circle10)sprintf(sendbuf,stopn);process=0;if(sendto(sockfd,sendbuf,sizeof(sendbuf),0,(structsockaddr*)&addr,sizeof(addr)0) perror(Tryingtosendto);exit(1); n=recvfrom(sockfd,recvbuf,sizeof(rec
93、vbuf),0,NULL,NULL);recvbufn=0;fputs(recvbuf,stdout);circle+;while(process);send.c把服务器的IP地址和端口号添加到IPv4的套接口地址结构中,以指明数据报将发往的目的地。在do-while循环中,使用sendto()发送了10条消息。然后使用recvfrom()读回服务器的数据应答,用fputs()输出到标准输出上。在recvfrorn()函数的最后,两个参数使用了空指针NULL,通知内核用户对信息来源不感兴趣。在这里使用空指针只是为了简单,这样做要承担一定的风险。因为,无论是同一主机或不同主机上的进程,都有可能给
94、客户的IP地址和端口发送数据报,而这些数据报如果被客户所接收,客户会误认为这是服务器的应答。接收UDP数据报的编程实例#include#include#include#include#includechar*host_name=127.0.0.1;/localhostintport=9889;voidmain()intcliaddr_len;charrecvbuf256,sendbuf256;intsockfd;structsockaddr_inservaddr,cliaddr;structhostent*shost_name;if(shost_name=gethostbyname(host_
95、name)=0)perror(Errorresolvinglocalhostn);exit(1);bzero(&servaddr,sizeof(servaddr);servaddr.sin_family=AF_INET;servaddr.sin_addr.s_addr=htonl(INADDR_ANY);servaddr.sin_port=htons(port);if(sockfd=socket(PF_INET,SOCK_DGRAM,0)=-1)perror(Erroropeningsocketn);exit(1);if(bind(sockfd,(structsockaddr*)&servad
96、dr,sizeof(servaddr)0)perror(calltobind);while(1)cliaddr_len=sizeof(cliaddr);if(recvfrom(sockfd,recvbuf,256,0,(structsockaddr*)&cliaddr,&cliaddr_len)=-1)perror(Errorinreceivingresponsefromservern);sprintf(sendbuf,Igotit.n);sendto(sockfd,sendbuf,256,0,(structsockaddr*)&cliaddr,cliaddr_len);printf(nRes
97、ponsefromserver:nn%sn,recvbuf);if(strncmp(recvbuf,stop,4)=0)break;close(sockfd);在这个程序中,socket()函数使用的第二个参数是SOCKDGRAM,表示要创建一个数据报套接口;然后装填IP地址和端口号,并调用bind()绑定套接口;在While循环中,调用函数recvfrom()等待接收到达服务器端口的数据报,然后调用sendto()函数发回应答信息。从while循环语句中可以判定出循环从不终止,直到接收到从客户进程发送来的stop信息才跳出循环,并关闭套接口。另外,这个UDP服务器是迭代的,而不是并发的。通常
98、,大多数TCP服务器是并发的,而大多数UDP服务器是迭代的。编译并运行编译并运行send.c和和receive.c程序,执行结果如下:程序,执行结果如下: $gcc -o send send.c $gcc -o receive receive.c先打开一个窗口,运行先打开一个窗口,运行receive接收程序,打开第二个窗口,运行接收程序,打开第二个窗口,运行send发送程序。第一个发送程序。第一个窗口:窗口:显示结果如下:显示结果如下: $ .receive Response from server: data packet with ID 0Response from server: dat
99、a packet with ID 1Response from server: data packet with ID 2Response from server: data packet with ID 10 stop $第二个窗口显示结果如下:第二个窗口显示结果如下: $./send I got it. I got it. I got it. $ 3 3 程序分析程序分析 虽然使用UDP协议节省了建立连接所带来的额外开销,但是程序receive()也有许多细节问题需要考虑。如果一个客户数据报丢失,客户将永远阻塞于recvfrom()的调用中,等待一个永远也不会到达的数据报。目前解决这个问题
100、的方法是给客户的recvfrom()设置一个超时。但在实际应用中仅仅设置一个超时也并不是一个十全十美的办法。因为用户无法判定超时的原因是数据报没有到达服务器,还是服务器的应答没有到达客户。这两种丢失是极不相同的。因此在UDP客户服务器程序中,必须能处理这些情况,来曾加程序执行的可靠性。这样一来就使得用户程序十分复杂。总之选择什么样的传输协议要根据具体传输环境来决定。67 RPC客户客户/服务器通信的概述服务器通信的概述对于网络通信,UNIX系统有若干种网络计算方法可以用来实现通信。RPC(remoteprocedurecalls,远程过程调用)作为一种编程接口,它的思想是:(1)客户端进程(即
101、发出请求的进程)调用一个通常称作客户存根(clientstub)的本地进程,其中包含网络通信的细节。(2)服务器端进程(处理请求的进程)有一个类似的服务器存根(serverstub),其中包含了它的网络通信细节。(3)客户和服务器都不需要了解底层的网络传输协议。(4)存根要使用某个协议编译器。协议编译器接受一个以C风格写成的协议定义文件,协议的定义文件包含远程过程的定义,它的参数的数据类型和它所返回的数据类型。67 RPC客户客户/服务器通信的概述服务器通信的概述当客户端调用一个RPC(生成了一个请求)时,它将等待服务器端的回应。由于客户端必须等待响应,需考虑以下的问题:(1)客户端应该为服务
102、器端的响应等待多久?RPC对待这个问题,通常使用一个默认的超时变量来限制客户端的等待时间。(2)如果客户端发出多个相同请求,服务器端应如何处理。问题的解答与特定程序相关,在某类操作(例如读请求)时,请求的活动事实上执行了几次。在其他的条件下,例如事务处理的请求只能执行一次。此时,软件要实现自己的管理程序。不过,RPC的定义独立于传输协议,如果一个RPC运行在可靠的传输层(TCP)之上,客户端只要收到了来自服务器端的回答就可以认为请求已被执行。(3)当进程位于不同的进程空间时如何实现变参的传递(传地址)?此外,客户端和服务器端进程还可能不在同一种系统之上,例如:SUN、VAX或IBM的平台。67
103、 RPC客户客户/服务器通信的概述服务器通信的概述为了解决这些问题,确保客户端和服务器端的进程可以使用RPC通信,进程间传输的数据要转换为独立的表示结构。Sun所使用的数据结构是XDR(外部数据表示)。客户端和服务程序的存根负责将传送的数据翻译成XDR格式和从XDR翻译回来。使用RPC的客户端进程和服务器端进程的关系如图所示。客户端进程服务器进程翻译成XDR格式转换成本地服务 表示请求响应转换成本地客户翻译成XDR格式端表示客户端存根服务器存根在在C语言中执行远程命令语言中执行远程命令rexec的语法概要:intrexec(char*ahost,unsignedshortinport,cons
104、tchar*user,constchar*passwd,constchar*cmd,int*fd2p);其中:ahost:是远程主机名的指针。这个指针通过rexec传给g6thostbyname网络调用来验证。inport:一个代表连接使用的端口的整数值。通常rexec使用的端口号是512(和远程命令执行相关的端口,使用TCP协议)user:用户名。passwd:相关口令。fd2p:一个整数的引用。如果该值不为0,rexec将认为它是某个有效文件描述符的引用,并把远程命令执行的错误输出映射到指定的文件上。/*使用rexec。第一命令行参数是执行远程命令的主机,第二个是传给远程主机的命令*/#i
105、nclude #include #include #include #include #include main(int argc, char *argv) int fd,count; char bufferBUFSIZ, *command, *host; if(argc!=3) fprintf(stderr, “Usage %s host commandn”,argv1); exit(1); host = argv1; command = argv2; if(fd = rexec(&host,htons(512), 0, 0, command, 0)=-1) fprintf(stderr,
106、“rexec failled”,argv1); exit(2); while(count = read(fd, buffer, BUFSIZ) 0) fwrite(buffer, count, 1 stdout); 上述程序的第一个命令行参数是执行远程命令的主机,命令行的第二个参数是要传给远程主机的命令。程序中用htons网络调用作为调用rexec函数的第一个参数,以保证在指定传输场四字节传输的正确顺序。htons的类型依赖于引人的头文件netinet/inh。代表用户名和口令的参数被设为0。这使rexec首先检测用户home目录中的.netrc文件中的用户名和口令,如果.netrc文件不存在
107、或不完整,rexec将中断以询问用户该信息。如rexec正确结束,将读入远程主机上的命令执行结果并显示于标准输出上。下面是程序的编译和运行结果。-morpheus%ccp1.c-op1-lsocket-lns1morpheus%p91stimpywhoname(simpy:gray):graypasswd(simpy:gray):bjeroszkconsoleApr1415:20bjeroszkpts/1Apr1415:21将本地函数转化为远程调用将本地函数转化为远程调用基于RPC的程序可以在分布式系统上运行,在这样的分布式设置中,包含被执行函数的服务器进程可以运行在与客户端不同的工作站上。将一个本地函数调用的程序酸化为远程过程调用的过程如下:(1)创建一个协议定义文件。该文件定义了远程过程的返回数据类型和参数类型,帮助系统了解哪个过程应于服务器端程序相联系。在使用RPC的时候,远程过程是服务器进程中的一部分。