第三章
网络编程
本章主要介绍一下网络编程的基本知识。由于书中后面章节都有一些简单的源程序实例来对各章的基本概念进行解释,因此必须具备必要的网络编程知识。
在平时工作中,为了查找安全漏洞,也需要编写一些短小精悍的程序来代替复杂的手工命令输入。
在操作系统一章中对Linux中的C语言编程和调试已经作了介绍。本章在前两章的基础上,首先对Linux中的网络编程作介绍,Linux对网络通信提供了很好的支持。由于Windows系统目前很流行,特别是开发环境Visual C++,所以,本章也对Windows环境下的网络编程作了介绍。
第一节Linux网络编程(Berkeley Sockets)
我们可以认为套接字是将Unix系统的文件操作推广到提供点对点的通信。如果要操作文件,应用程序会根据应用程序的需要为之创建一个套接字。操作系统返回一个整数。应用程序通过引用这个正数来使用这个套接字。文件描述符和套接字描述符的不同点在于,在程序调用open()时,操作系统将一个文件描述符绑定到一个文件或设备,但在创建一个套接字时,可以不将它绑定到一个目标地址。程序可以在任何想要用这个套接字的时候指定目标地址。
在点对点的通信程序中,我们将请求服务或数据的程序叫做客户端程序,提供数据或服务的软件叫做服务器程序。
图1是一个面向连接的服务器程序和客户端程序的流程图。
对于使用无连接协议的服务器程序和客户端程序的流程,请参见图2。图中,客户端程序并不和服务器程序建立连接,它是通过使用服务器地址作为参数的sendto()系统调用,发送一个数据报给服务器的。同样,服务器并不接受客户端的连接,而是用recvfrom()调用等待从客户端来的数据。
套接字系统调用
下面解释一下几个基本的套接字系统调用函数。只要你将下面的函数与系统的输入输出函数调用加以对比,就能很快地掌握这些函数调用了。
socket()
------------------------------------------------------------
#include < sys/types.h>
#include < sys/socket.h>
int socket(int family, int type, int protocol);
------------------------------------------------------------
int family参数指定所要使用的通信协议,取以下几个值。
值 含义
AF_UNIX Unix内部协议
AF_INET Internet协议
AF_NS Xerox NS协议
AF_IMPLINK IMP 连接层
int type 指定套接字的类型,取以下几个值
值 含义
SOCK_STREAM 流套接字
SOCK_DGRAM 数据报套接字
SOCK_RAW 未加工套接字
SOCK_SEQPACKET 顺序包套接字
int protocol 参数通常设置为0。
socket()系统调用返回一个整数值,叫做套接字描述字sockfd,它的原理与文件描述符一样。网络I/O的第一步通常就是调用这个函数。
socektpair()
------------------------------------------------------------
#include < sys/types.h>
#include < sys/socket.h>
int socketpair(int family, int type, int protocol, int sockvec[2]);
------------------------------------------------------------
这个调用返回两个套接字描述符, sockvec[0]和sockvec[1],它们没有名字,但是连着的。这个调用与管道系统调用类似。由这个调用创建的结构叫做一个流管道。
bind()
------------------------------------------------------------
#include < sys/types.h>
#include < sys/socket.h>
int bind(int sockfd, struct sockaddr *myaddr, int addrlen);
------------------------------------------------------------
这个调用将一个名字命名给一个没有名字的套接字。第二个参数myaddr是指向一个特定协议地址的指针,第三个参数是这个地址结构的大小。
bind()有三个作用:
服务器在系统里登记它们的地址
客户为它自己注册一个地址
一个没有连接的客户确保系统固定分配给它一个唯一的地址
connect()
------------------------------------------------------------
#include < sys/types.h>
#include < sys/socket.h>
int connect(int sockfd, struct sockaddr *servaddr, int addrlen);
------------------------------------------------------------
这个过程在socket()调用后,将一个套接字描述符和一个与服务器建立的连接的联系。sockfd是一个由socket()调用返回的套接字描述符。第二个参数是服务器套接字地址的指针,第三个参数是这个地址的长度。
listen()
------------------------------------------------------------
#include < sys/types.h>
#include < sys/socket.h>
int listen(int sockfd, int backlog)
------------------------------------------------------------
面向连接的服务器使用这个系统调用,来表示它希望接受连接。
这个系统调用通常在socket()和bind()之后,在accept()调用之前调用。参数backlog表示当它们等待执行accept()系统调用之前,系统能对多少个连接请求进行排队。
accept()
------------------------------------------------------------
#include < sys/types.h>
#include < sys/socket.h>
int accept(int sockfd, struct sockaddr *peer, int *addrlen);
------------------------------------------------------------
在一个建立好连接的服务器执行了listen()系统调用之后,一个实际和客户的连接过程等待服务器调用accept()系统调用。
accept()取出在队列里的第一个连接请求,并且创建另一个和sockfd有相同属性套接。如果队列中没有连接请求,这个调用就将调用者阻塞,知道有请求为止。
peer和addrlen 参数用来返回连接的客户的地址。调用者在调用之前设置addrlen的值,系统调用通过它返回一个值。
send(), sendto(), recv(), recvfrom()
------------------------------------------------------------
#include < sys/types.h>
#include < sys/socket.h>
int send(int sockfd, char *buff, int nbytes, int flags);
int sendto(int sockfd, char *buff, int nbytes, int flags,
struct sockaddr *to, int addrlen);
int recv(int sockfd, char *buff, int nbytes, int flags);
int recvfrom(int sockfd, char *buff, int nbytes, int flags,
struct sockaddr *from, int addrlen);
------------------------------------------------------------
这些调用与标准的系统调用read()和write()相似。
这些调用需要附加的参数。Flag参数可以是0或者下列常数:
MSG_OOB 接受或发送绑定外的数据
MSG_PEEK 监视进入信息
MSG_DONTROUTE 绕过路由
close()
------------------------------------------------------------
#include < sys/types.h>
#include < sys/socket.h>
int close(int sockfd);
------------------------------------------------------------
关闭一个套接字。
编程实例
从一个描述符读n字节数据
/* 从一个描述符读n字节数据 */
int readn(register int fd, register char *ptr, register int nbytes)
{
int nleft, nread;
nleft=nbytes;
while (nleft > 0){
nread=read(fd,ptr,nleft);
if(nread < 0)
return(nread);
else if (nread==0)
break;
nleft-=nread;
ptr +=nread;
}
return(nbytes - nleft);
}
写n字节数据到一个描述符
/* 写n字节数据到一个描述符 */
int writen(register int fd, register char *ptr, register int nbytes)
{
int nleft, nwritten;
nleft=nbytes;
while(nleft>0){
nwritten=write(fd,ptr,nleft);
if(nwritten< =0)
return(nwritten);
nleft -= nwritten;
ptr += nwritten;
}
return(nbytes-nleft);}
TCP编程
/* inet.h
* 服务器和客户端程序的头文件。
*/
#include < stdio.h>
#include < sys/types.h>
#include < sys/socket.h>
#include < netinet/in.h>
#include < arpa/inet.h>
#define SERV_UDP_PORT 6000
#define SERV_TCP_PORT 6000
#define SERV_HOST_ADDR "192.43.235.6" /* host addr for server */
char *pname;
服务器程序如下:
/* TCP服务器程序 */
#include "inet.h"
main(int argc, char * argv)
{
int sockfd, newsockfd, clilen, childpid;
struct sockaddr_in cli_addr, serv_addr;
pname = argv[0];
/* 打开一个TCP套接字 (一个Internet流套接字) */
if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
err_dump("server: can't open stream socket");
/* 绑定本地地址,这样,客户机就能访问到服务器。*/
bzero((char *) &serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(SERV_TCP_PORT);
if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0)
err_dump("server: can't bind local address");
listen(sockfd, 5);
for ( ; ; ) {
/* 等待一个来自客户机的连接进程,这是一个并发的服务器。*/
clilen = sizeof(cli_addr);
newsockfd = accept(sockfd, (struct sockaddr *) &cli_addr, &clilen);
if (newsockfd < 0)
err_dump("server: accept error");
if ( (childpid = fork()) < 0)
err_dump("server: fork error");
else if (childpid == 0) { /* 子进程 */
close(sockfd); /* 关闭原来的套接字 */
str_echo(newsockfd); /* 处理请求 */
exit(0);
}
close(newsockfd); /* 父进程 */
}
}
服务机代码:
/* 使用TCP协议客户机 */
#include "inet.h"
main(argc, argv)
int argc;
char *argv[];
{
int sockfd;
struct sockaddr_in serv_addr;
pname = argv[0];
/* 在结构"serv_addr"里填入想要连接的服务器的地址*/
bzero((char *) &serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(SERV_HOST_ADDR);
serv_addr.sin_port = htons(SERV_TCP_PORT);
/* 打开一个TCP套接字(一个Internet 流套接字) */
if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
err_sys("client: can't open stream socket");
/* 连到服务器上*/
if (connect(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0)
err_sys("client: can't connect to server");
str_cli(stdin, sockfd); /* 全部输出 */
close(sockfd);
exit(0);
}
套接字和信号量
在使用一个套接字时,可以产生三个信号量。
(SIGIO) 这个信号量表示一个套接字已经准备好进行异步I/O了。这个信号量会发给这个套接字的所有进程。这些进程是通过用FIOSETOWN 或 SIOCSPGRP 调用ioctl而建立的。或者是用F_SETOWN调用fcntl建立的。这个信号量只在这个进程在这个套接字上,用FIOASYNC调用ioctl或用FASYNC调用fcntl后,可以进行异步I/O后发给这些进程的。
(SIGURG) 这个信号量表示出现了一个紧急情形。一个紧急情形是任何一个在套接字上一个出现了一个超过带宽的数据的到达信息。超过带宽表示在用户进程到达的数据超出了I/O缓冲区了。
(SIGPIPE) 这个信号量表明我们不再会向套接字,管道或FIFO写数据了。
异步I/O
异步I/O允许进程通知操作系统内核,如果一个指定的描述符可以进行I/O时,内核通知该进程。这通常叫做信号量驱动I/O。内核通知进程的信号量是SIGIO。
为了实现异步I/O,一个进程必须:
建立一个处理SIGIO信号量的程序。
将进程ID或进程组ID设置好,能接受SIGIO信号量。这是由fcntl命令实现的。
进程必须用dcntl系统调用,激活异步I/O。
第二节Windows网络编程(WinSock)
这里介绍WinSock创建TCP流套接字程序。Winsock的编程和第一部分将的非常的相似。
创建TCP流套接字服务器程序
用socket()函数打开一个流套接字。用AF_INET指定地址格式参数,SOCK_STREAM指定类型参数。
if ((WinSocket = socket (AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET)
{
wsprintf (szError, TEXT("Allocating socket failed. Error: %d"),
WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Error"), MB_OK);
return FALSE;
}
使用SOCKADDR_IN结构作为地址参数,用bind()函数命名套接字。
用socket()函数打开一个套接字时,这个套接字没有名字,仅仅在一个地址家族名字空间里分配了一个描述符。为了让客户端套接字区分开来,一个TCP流套接字服务器程序必须命名它的套接字。但不必用bind()函数命名客户端的套接字。
一个套接字的名字在TCP/TP协议里由三部分组成:协议名称,主机地址和一个表征应用程序的端口数字。这些地址域sin_family, sin_addr, sin_port都是SOCKADDR_IN结构的成员。必须在调用bind()之前初始化SOCKADDR_IN结构。
下面的这段代码示范怎样初始化SOCKADDR_IN结构和调用bind()函数。
// 填写本地套接字地址数据
local_sin.sin_family = AF_INET;
local_sin.sin_port = htons (PORTNUM);
local_sin.sin_addr.s_addr = htonl (INADDR_ANY);
// 将本地地址和WinSocket相连
if (bind (WinSocket,
(struct sockaddr *) &local_sin,
sizeof (local_sin)) == SOCKET_ERROR)
{
wsprintf (szError, TEXT("Binding socket failed. Error: %d"),
WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Error"), MB_OK);
closesocket (WinSocket);
return FALSE;
}
使用listen()函数侦听。为了准备一个TCP流套接字服务器的一个名字连接,必须侦听从客户端来的连接。
下面这个例子说明了怎样使用listen()函数。
if (listen (WinSocket, MAX_PENDING_CONNECTS) == SOCKET_ERROR)
{
wsprintf (szError,
TEXT("Listening to the client failed. Error: %d"),
WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Error"), MB_OK);
closesocket (WinSocket);
return FALSE;
}
使用accept()接受客户端的连接。
TCP流服务器套接字使用这个函数来完成服务器和客户端的名字连接过程。
Accept()函数创建一个新的套接字。初始的由服务器打开的套接字继续侦听该端口,可以一直接受连接,知道关闭。服务器程序必须负责关闭侦听套接字以及在接受客户连接是创建的所有套接字。
下面的代码是accept()函数应用的示范。
accept_sin_len = sizeof (accept_sin);
// 接受一个试图在WinSocket上连接的请求
ClientSock = accept (WinSocket,
(struct sockaddr *) &accept_sin,
(int *) &accept_sin_len);
// 停止对客户连接的侦听
closesocket (WinSocket);
if (ClientSock == INVALID_SOCKET)
{
wsprintf (szError, TEXT("Accepting connection with client
failed.") TEXT(" Error: %d"), WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Error"), MB_OK);
return FALSE;
}
使用send() and recv()函数发送和接受客户的数据。
一旦客户端和服务端的套接字连接上后,就能使用上述两个函数交换数据。
Send()函数将数据输出到套接字上。Recv()函数从套接字中读取数据。
下面的代码是上述两个函数的应用示范。
for (;;)
{
// 从客户端接受数据
iReturn = recv (ClientSock, szServerA, sizeof (szServerA), 0);
// 确认数据收到后,显示数据
if (iReturn == SOCKET_ERROR)
{
wsprintf (szError, TEXT("No data is received, receive failed.")
TEXT(" Error: %d"), WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Server"), MB_OK);
break;
}
else if (iReturn == 0)
{
MessageBox (NULL, TEXT("Finished receiving data"),
TEXT("Server"), MB_OK);
break;
}
else
{
// 将ASCII字符串转换成Unicode字符串
for (index = 0; index < = sizeof (szServerA); index++)
szServerW[index] = szServerA[index];
// 显示从客户端接收到的数据
MessageBox (NULL, szServerW, TEXT("Received From Client"),
MB_OK);
}
}
// 从服务器给客户端发个数据
if (send (ClientSock, "To Client.", strlen ("To Client.") + 1, 0)
== SOCKET_ERROR)
{
wsprintf (szError,
TEXT("Sending data to the client failed. Error: %d"),
WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Error"), MB_OK);
}
成功地完成send()函数的调用并不能说明数据的发送是成功的。
使用closesocket()函数来断开连接。当服务器和客户端数据交换结束后,使用这个函数关闭套接字。为了在一个TCP连接确认数据已经交换了,一个程序应该在调用这个函数之前调用shutdown()函数。
一个程序应该在程序结束前,关闭所有打开的程序,以便将套接字资源返回给操作系统。对于TCP流套接字,当一个套接字连接结束后,服务器关闭了有accept()创建的套接字,但最先的侦听套接字还是打开的。在程序结束前要将侦听套接字也关闭。
创建TCP流套接字客户端程序
用socket()函数打开一个流套接字。 调用这个函数时使用AF_INET作为地址格式参数,用SOCK_STREAM做类型参数。
用SOCKADDR_IN结构作为名字参数调用connect()函数和服务器连接。TCP流套接字客户端通过这个函数将名字和服务器相连。
在调用connect()函数之前要初始化SOCKADDR_IN 结构,这和bind()函数调用类似,但是sin_port 和sin_addr用远程的套接字名字,而不是本地的。
下面这段代码显示怎样和服务器相连。
// 建立一个和服务器套接字的连接
if (connect (ServerSock,
(PSOCKADDR) &destination_sin,
sizeof (destination_sin)) == SOCKET_ERROR)
{
wsprintf (szError,
TEXT("Connecting to the server failed. Error: %d"),
WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Error"), MB_OK);
closesocket (ServerSock);
return FALSE;
}
用send()和recv*(函数和服务器交换数据。用closesocker()函数关闭连接。
第三节MFC中的编程
Visual C++的MFC提供了CSocket类用来实现网络通信。下图给出了CSocket 类的继承关系。
下面介绍VC++在Windows 95中实现Socket的 CSocket 类相关成员函数(这些成员函数实际上是从CAsyncSocket 类继承来的)的使用。
(1) BOOL Create( UINT nSocketPort = 0, int nSocketType = SOCK_STREAM, long lEvent = FD_READ |FD_WRITE|FD_OOB|FD_ACCEPT|FD_CONNECT| FD_CLOSE,LPCTSTR lpszSocketAddress = NULL )
该函数用来建立Socket。 其中,nSocketPort 为所选择的Socket 端口,一般要大于 1023, 如果该参数为0, 则由系统选定一端口,默认值为0 ;nSocketType 为套接字类型:SOCK_STREAM 表示为流套接字,SOCK_DGRAM 表示为数据报套接字,默认值为SOCK_STREAM ;lEvent 标识该Socket 要完成哪种工作,默认值为FD_READ|FD_WRITE|FD_OOB| FD_ACCEPT|FD_CONNECT|FD_CLOSE ;lpszSockAddress 为网络地址信息结构指针,包含网络地址, 默认值为NULL 。
(2)BOOL Bind( UINT nSocketPort, LPCTSTR lpszSocketAddress = NULL )
该函数的作用是将Socket 端口与网络地址连接起来。参数含义同上 。
(3)BOOL Listen( int nConnectionBacklog = 5 )
该函数的作用是等待Socket请求。其中,nConnec-tionBacklog 表示等待队列的长度,默认值为最大值5 。
(4)virtual BOOL Accept( CAsyncSocket& rConnectedSocket, SOCKADDR* lpSockAddr = NULL, int* lpSockAddrLen = NULL )
该函数的作用是取得队列上第一个连接请求并建立一个具有与Socket相同特性的套接字。其中,rConnectedSocket 表示一个新的Socket 。
(5)BOOL Connect( LPCTSTR lpszHostAddress, UINT nHostPort )
该函数的作用是提出请求。其中,lpszHostAddress 和 nHostPort 为接受请求进程的网络地址和Socket 端口号。
(6)virtual void Close( )
该函数的作用是关闭该Socket 。
利用CSocket类直接进行数据通信有两种方式:一种是利用CSocketFile 类和Archive 类去实现,另一种是利用CSocket的成员函数 Receive、Send、ReceiveFrom、SendTo、Listen 和 Accept 等来实现(这些成员函数实际上也是从CAsyncSocket 类继承的)。
两种方法的实现步骤如下 :
Server : Construct-> Creat-> Bind -> Listen-> Accept-> Send->Close ;
Cilent : Construct ->Creat-> Connect-> Receive-> Close。
下面就用VC++的代码分别介绍如何运用上述两种方法来实现Socket 编程。
1、 利用CSocketFile类和Archive 类实现
(1)服务器程序流程
// 创建一个套接字对象
CSocket sockSrvr;
//为上述套接字对象创建一个套接字
sockSrvr.Create(nPort);
//开始侦听
sockSrvr.Listen( );
//创建一个新的套接字对象
CSocket sockRecv;
//接受连接
sockSrvr.Accept( sockRecv );
// 创建文件对象
CSocketFile file(&sockRecv);
//创建一个archive对象
CArchive arIn(&file, CArchive::load);
/*or*/_CArchive arOut(&file, CArchive::store);
//使用archive对象传输数据
arIn >> dwValue;
/*or*/ arOut < < dwValue;
(2)客户端程序流程
//创建一个套接字对象
CSocket sockClient;
//为这个对象创建一个套接字
sockClient.Create( );
//寻找一个连接
sockClient.Connect(strAddr, nPort);
//创建一个文件对象
CSocketFile file(&sockClient);
//创建一个archive对象
CArchive arIn(&file, CArchive::load);
/*or*/_CArchive arOut(&file, CArchive::store);
//使用这个对象传输数据
arOut < < dwValue;
/*or*/ arIn >> dwValue;
上述程序中, nPort 是Socket 的端口号,strAddr 是该机器的IP地址(如202.197.1.3 或 FTP://RedAlert.com等),这两个变量在Server和Client中要一致。当Server进程运行至Listen 后便处于睡眠状态直到Client进程执行Connect 时才被唤醒,而后两个进程便开始传输数据了。
2、利用CSocket的成员函数实现
(1)服务器流程
//套接字初始化
if(!AfxSocketInit()){
MessageBox("WindowsSocket initial failed!","Send",MB_ICONSTOP);
Return;
}
// 创建两个套接字对象
CSocket ChatSend,server;
// 创建一个套接字
if(!ChatSend.Create(nPort)) // nPort=1025
MessageBox("SendSocket create failed!", "Send",MB_ICONSTOP);
else{
// 把本地地址给套接字
ChatSend.Bind(nProt,strAddr);
// strAddr="202.196.111.1"
// 开始侦听
ChatSend.Listen();
// 创建一个新的套接字并和他相连
ChatSend.Accept(Server);
}
//发送一个CString 对象
Server.SendTo(csSendText,csCounts,nPort,strAddr);
// 关闭这两个套接字
Server.Close();
ChatSend.Close();
(2)客户端程序流程
// 套接字初始化
if(!AfxSocketInit()){
MessageBox("WindowsSocket initial failed!", "Receive",MB_ICONSTOP);
return;
}
// 创建一个套接字对象
CSocket ChatRecieve;
// 创建一个套接字
if(!ChatReceive.Create()){
MessageBox("ReceiveSocket create failed!","Receive",MB_ICONSTOP);
return;
}
else{
// 创建一个对等套接字
ChatReceive.Connect(strAddr,nPort);
}
//接受一个CString 对象
ChatReceive.ReceiveFrom(csReceiveText,csCounts,strAddr,nPort);
// 关闭套接字
ChatReceive.Close();
上述两个进程完成的工作是:由Server 进程发送一字符串,Client 进程接收。 strAddr 和 nPort 的含义与方法1 中的相同 ;csSendText 和 csReceiveText 为发送与接收的字符串;csCounts为字串长度,这一长度在两个进程中要求接收长度小于或等于发送长度,否则会导致数据传输错误。另外,在程序中要加入头文件afxsock.h, CSocket 类的有关说明均在afxsock.h 中。
方法1 适合于对多个不同类型数据的通信,方法2 适合于对字符串的通信,具体选用何种方法则取决于具体应用的需求。