今天刚刚做的系统 不小心删了系统文件按了end 现在开不了机了怎么搞

本章主要介绍一下网络编程的基本知识。由于书中后面章节都有一些简单的源程序实例来对各章的基本概念进行解释,因此必须具备必要的网络编程知识。 在平时工作中,为了查找安全漏洞,也需要编写一些短小精悍的程序来代替复杂的手工命令输入。 在操作系统一章中对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,
nleft= while (nleft & 0){ nread=read(fd,ptr,nleft); if(nread & 0) return(nread); else if (nread==0) nleft-= ptr += } return(nbytes – nleft); }
写n字节数据到一个描述符 /* 写n字节数据到一个描述符 */ int writen(register int fd, register char *ptr, register int nbytes) { int nleft,& nleft= while(nleft&0){ nwritten=write(fd,ptr,nleft); if(nwritten& =0) return(nwritten); nleft -= ptr += } 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 */
服务器程序如下: /* TCP服务器程序 */ #include “inet.h”
main(int argc, char * argv) { int sockfd, newsockfd, clilen, struct sockaddr_in cli_addr, serv_
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) char *argv[]; { struct sockaddr_in serv_
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); } else if (iReturn == 0) { MessageBox (NULL, TEXT(“Finished receiving data”),& TEXT(“Server”), MB_OK); } 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 sockS
  //为上述套接字对象创建一个套接字
  sockSrvr.Create(nPort);
  //开始侦听   sockSrvr.Listen( );
  //创建一个新的套接字对象   CSocket sockR
  //接受连接   sockSrvr.Accept( sockRecv );
// 创建文件对象 CSocketFile file(&sockRecv);
  //创建一个archive对象   CArchive arIn(&file, CArchive::load);
  /*or*/_CArchive arOut(&file, CArchive::store);
  //使用archive对象传输数据   arIn && dwV
  /*or*/ arOut & & dwV
  (2)客户端程序流程   //创建一个套接字对象   CSocket sockC
  //为这个对象创建一个套接字   sockClient.Create( );
  //寻找一个连接   sockClient.Connect(strAddr, nPort);
  //创建一个文件对象   CSocketFile file(&sockClient);
  //创建一个archive对象   CArchive arIn(&file, CArchive::load);
  /*or*/_CArchive arOut(&file, CArchive::store);
  //使用这个对象传输数据   arOut & & dwV
  /*or*/ arIn && dwV
  上述程序中, 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);&    R&   }
  // 创建两个套接字对象   CSocket ChatSend,
  // 创建一个套接字   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);&   &   }
  // 创建一个套接字对象   CSocket ChatR
  // 创建一个套接字   if(!ChatReceive.Create()){&    MessageBox(“ReceiveSocket create failed!”,”Receive”,MB_ICONSTOP);&   &   }&   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 适合于对字符串的通信,具体选用何种方法则取决于具体应用的需求。
&— zwell @ 2:28 am
关于Socket编程,在《Linux从入门到精通》里有简单的介绍,更详细的可以参考《UNIX网络编程 卷1:联网的API:套接字与XTI 第2版》清华影印版,其中还讲了线程(Thread)编程。极好的参考书,可惜没人把它翻译过来。胡淑瑜翻译了一篇,我把它收集进来了。如有更新,请参考胡先生的。&
________________________________________________________________________________ | 版权声明 | | | | 1、本文可以转载、修改及引用,但请保留本声明和其后所付英文原文。 | | 2、任何情况下,作者和译者姓名不可删除。 | | 3、任何情况下,本文不可进入商业领域。 | | | | 胡淑瑜 | | (husuyu@linux.cqi.com.cn) | | | | 1998年11月 | |______________________________________________________________________________| 第59章 目录 网络程序设计 端口(Ports)和套接字(Sockets) 套接字程序设计 socket()系统调用(System Call) bind()系统调用 listen()系统调用 accept()系统调用 setsockopt和getsockopt系统调用 connect()系统调用 程序清单59.1服务器端(Server Side)面向套接字(socket-oriented)协议 程序清单59.2客户端函数(The lient Side function) 无连接(Connectionless)套接字程序设计 程序清单59.3服务端 注意 记录(Record)和文件锁定(Locking) 进程间通信 小结 ------------------------------------------------------------------------------- --第59章-- 网络程序设计 作者 Kamran Husain,Tim Parker 译者 胡淑瑜 本章内容 端口和套接字 套接字程序设计 记录和文件锁定 进程间通信 阅读本章需你具有如下网络程序设计的基本概念 端口和套接字 记录和文件锁定 进程间通信 本文不可能在几页之内就能与你说清网络程序设计的全部内容.事实上,一本第一卷就有 800页的专门描述网络程序设计的参考书是最有用的.如果你真想进行网络编程的话,你需要 具有编译器,TCP/IP和网络操作系统的大量经验.另外,你还需有极大的耐心. 欲知TCP/IP详细内容,请参见Tim Parker所著之&&自学TCP/IP十四天&& (Sams Publish- ing). 端口和套接字 网络程序设计全靠套接字接受和发送信息.尽管套接字这个词好象显得有些神秘,但其实 这个概念极易理解. 大多数网络应用程序使用两个协议:传输控制协议(TCP)和用户数据包协议(UDP).他们都 使用一个端口号以识别应用程序.端口号为主机上所运行之程序所用,这样就可以通过号码 象名字一样来跟踪每个应用程序.端口号让操作系统更容易的知道有多少个应用程序在使用 系统,以及哪些服务有效. 理论上,端口号可由每台主机上的管理员自由的分配.但为了更好的通信通常采用一些约 定的协议.这些协议使能通过端口号识别一个系统向另一个系统所请求的服务的类型.基于 如此理由,大多数系统维护一个包含端口号及它们所提供哪些服务的文件. 端口号被从1开始分配.通常端口号超出255的部分被本地主机保留为私有用途.1到255之 间的号码被用于远程应用程序所请求的进程和网络服务.每个网络通信循环地进出主计算机 的TCP应用层.它被两个所连接的号码唯一地识别.这两个号码合起来叫做套接字.组成套接 字的这两个号码就是机器的IP地址和TCP软件所使用的端口号. 因为网络通讯至少包括两台机器,所以在发送和接收的机器上都存在一个套接字.由于每 台机器的IP地址是唯一的,端口号在每台机器中也是唯一的,所以套接字在网络中应该是唯 一的.这样的设置能使网络中的两个应用程序完全的基于套接字互相对话. 发送和接收的机器维护一个端口表,它列出了所有激活的端口号.两台机器都包括一个进 程叫做绑定,这是每个任务的入口,不过在两台机器上恰恰相反.换句话说,如果一台机器的 源端口号是23而目的端口号被设置成25,那么另一台机器的源端口号设置成25目的端口号设 置成23. 套接字程序设计 Linux支持伯克利(BSD)风格的套接字编程.它同时支持面向连接和不连接类型的套接字. 在面向连接的通讯中服务器和客户机在交换数据之前先要建立一个连接.再不连接通讯中数 据被作为信息的一部分被交换.无论那一种方式,服务器总是最先启动,把自己绑定(Banding )在一个套接字上,然后侦听信息.服务器究竟怎样试图去侦听就得依靠你编程所设定的连接 的类型了. 你需要了解一些系统调用 socket() bind() listen() accept() setsockopt()和getsockopt() connect() sendto() recvfrom() 我们将在以下的例子中使用这些系统调用. socket()系统调用 socket()系统调用为客户机或服务器创建一个套接字,套接字函数在如下定义: #include&sys/types.h& #include&sys/socket.h& int socket(int family, int type, int protocol) 在Linux中family=AF_UNIX.type可以是SOCK_STREAM它是可靠的虽然通讯速度较慢,也可 以是SOCK_DGRAM它通讯速度较快但不可靠.如果type=SOCK_STREAM那么protocol=IPPROTO_ TCP.如果type=SOCK_DGRAM那么protocol=IPPROTO_UDP. 如果出错,函数将返回-1.否则返回一个套接字描述符你可以在程序后面的调用中通过套 接字描述符使用这个套接字. 套接字创建时没有指定名字.客户机用套接字的名字读写它.这就是如下绑定函数所要做 之事. bind()系统调用 bind()系统调用为没有名字的套接字分配一个名字.绑定函数是这样定义的: #include&sys/types.h& #include&sys/socket.h& int bind(int sockfd, struct sockaddr *saddr, int addrlen) 第一个参数是一个套接字描述符.第二个参数是名字所用的一个结构.第三个参数是结构 的大小. 现在你已经为你的客户机或服务器限定了一个地址,你可以connect()服务器或是在服务 器上侦听了.如果你的程序是一个服务器,那么它把自己设置为侦听然后等待连接.让我们来 看看哪些函数能让你试图这样做. listen()系统调用 listen()系统调用被服务器所使用.下面有它的定义: #include&sys/types.h& #include&sys/socket.h& int listen(int sockfd, int backlog); sockfd是套接字描述符.backlog是在一时间内尚未被决定是否拒绝的连接的号码.一般 使用标准值5.如发生错误则返回值小于1. 如果这个调用成功,你就已经可以接受连接了. accept()系统调用 accept()调用被服务器用于接受任何从客户机connect()调用所引入的信息.必须明白的 是,如果没有接受到连接这个函数将不返回任何值.它是象这样定义的: #include&sys/types.h& #include&sys/socket.h& int accept(int sockfd, struct sockaddr *peeraddr, int addrlen) 除peeraddr指向发出连接请求的客户机的信息外,其它参数和bind()系统调用的相同.在 信息引入的基础上,peeraddr所指向的结构的域被填上相应的值. setsockopt()和getsockopt()系统调用 Linux所提供的socket库含有一个错误(bug).此错误表现为你不能为一个套接字重新启 用同一个端口号,即使在你正常关闭该套接字以后.例如,比方说,你编写一个服务器在一个 套接字上等待的程序.服务器打开套接字并在其上侦听是没有问题的.无论如何,总有一些原 因(不管是正常还是非正常的结束程序)使你的程序需要重新启动.然而重启动后你就不能把 它绑定在原来那个端口上了.从bind()系统调用返回的错误代码总是报告说你试图连接的端 口已经被别的进程所绑定. 问题就是Linux内核在一个绑定套接字的进程结束后从不把端口标记为未用.在大多数UN IX系统中,端口可以被一个进程重复使用,甚至可以被其它进程使用. 在Linux中绕开这个问题的办法是,但套接字已经打开但尚未有连接的时候用setsockopt ()系统调用在其上设定选项(options).setsockopt()调用设置选项而getsockopt()从给定 的套接字取得选项. 这里是这些调用的语法: #include&sys/types.h& #include&sys/socket.h& int getsockopt(int sockfd, int level, int name , char *value, int *optlen) int setsockopt(int sockfd, int level, int name , char *value, int *optlen) sockfd必须是一个已打开的套接字.level是函数所使用的协议标准(protocol level)(T CP/IP协议使用IPPROTO_TCP,套接字标准的选项实用SOL_SOCKET),选项的名称(name)在套接 字说明书中(man page)有详细说明.*value指向为getsockopt()函数所获取的值或setsocko pt()函数所设置的值的地址.optlen指针指向一个整数,该整数包含参数以字节计算的长度. 其值被getsockopt()设置且其值必须被程序员设定当使用一个经由setsockopt(). 选项的所有细节可以在使用手册中setsockopt的第二节(setsockopt(2))找到. 现在我们再回到Linux的错误上来.当你打开一个套接字时必须同时用下面的代码段来调 用setsockopt()函数: #ifdef LINUX opt = 1; len = sizeof(opt); setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&opt ,&len); #endif 只有当你想让程序不光是在Linux系统下使用时,#ifdef和#endif描述才是必须的.有些U NIX系统可能不支持或不需要SO_REUSEADDR标志. connect()系统调用 connect()调用被在面向连接的系统中客户机连接服务器时使用.connect()调用必须被 用在bind()调用之后.它是这样定义的: #include&sys/types.h& #include&sys/socket.h& int connect(int sockfd, struct sockaddr *servs addr, int addrlen) 除servsaddr外所有的参数都和bind调用相同,servsaddr指向客户机所连接的服务器的 信息.当接到请求后,accept调用为服务器创建一个新的套接字.然后服务器就可以fork()一 个新进程然后再去等待其它连接.在服务器端你可以象程序清单59.1所显示的那样编写代码 程序清单59.1 面向套接字协议的服务器端 #include &sys/types.h& #include &sys/socket.h& #include &linux/in.h& #include &linux/net.h& #define MY_PORT 6545 main(int argc, char *argv[]) { int sockfd, /* child id */ struct sockaddr_ struct sockaddr_in clientI if ((sockfd = socket(AF_INET, SOCK_STREAM, 0) & 0) { myabort("Unable to create socket"); } #ifdef LINUX opt = 1; len = sizeof(opt); setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&opt,&len); #endif bzero((char *)&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_family = htons(MY_PORT); /* * The htonl (for a long integer) and htons (for short integer) convert * a host oriented byte order * into a network order. */ if (bind(sockfd,(struct sockaddr *)&servaddr,sizeof(struct sockaddr)) & 0) { myabort("Unable to bind socket"); } listen(sockfd,5); for (;;) { /* wait here */ newfd=accept(sockfd,(struct sockaddr *)&clientInfo, sizeof(struct sockaddr); if (newfd & 0) { myabort("Unable to accept on socket"); } if ((cpid = fork()) & 0) { myabort("Unable to fork on accept"); } else if (cpid == 0) { /* child */ close(sockfd); /* no need for original */ do_your_thing(newfd); exit(0); } close(newfd); /* in the parent */ } 在面向连接的协议的程序中,服务器执行以下函数: 调用socket()函数创建一个套接字. 调用bind()函数把自己绑定在一个地址上 调用listen()函数侦听连接 调用accept()函数接受所有引入的请求 调用read()函数获取引入的信息然后调用write()回答 现在让我们来看看客户端所要做的事情,见程序清单59.2. 程序清单59.2 客户端函数 #include &sys/types.h& #include &sys/socket.h& #include &linux/in.h& #include &linux/net.h& #define MY_PORT 6545 #define MY_HOST_ADDR "204.25.13.1" int getServerSocketId() { int fd, struct sockaddr_in unix_ /* create a Unix domain stream socket */ if ( (fd = socket(AF_UNIX, SOCK_STREAM, 0)) & 0) { return(-1); } #ifdef LINUX opt = 1; len = sizeof(opt); setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&opt,&len); #endif /* fill socket address structurew/our address */ memset(&unix_addr, 0, sizeof(unix_addr)); unix_addr.sin_family = AF_INET; /* convert internet address to binary value*/ unix_addr.sin_addr.s_addr = inet_addr(MY_HOST_ADDR); unix_addr.sin_family = htons(MY_PORT); if (bind(fd, (struct sockaddr *) &unix_addr, len) & 0) return(-2); memset(&unix_addr, 0, sizeof(unix_addr)); if (connect(fd, (struct sockaddr *) &unix_addr, len) & 0) return(-3); return(fd); } 在面向连接的通信中客户机要做如下一些事: 调用socket()函数创建一个套接字 调用connect()函数试图连接服务器 如果连接成功调用write()函数请求数据,调用read()函数接收引入的应答 不连接(Connectionless)套接字程序设计 现在让我们来考虑一下不连接的信息交换.其服务器端的原理和面向连接的协议有所不 同.服务器并不调用listen和accept而是调用recvfrom().同样,服务器用sendto()函数来 应答信息.服务器端程序见程序清单59.3. 程序清单59.3 服务器端 #include &sys/types.h& #include &sys/socket.h& #include &linux/in.h& #include &linux/net.h& #define MY_PORT 6545 #define MAXM 4096 char mesg[MAXM]; main(int argc, char *argv[]) { int sockfd, /* child id */ struct sockaddr_ struct sockaddr_in clientI if ((sockfd = socket(AF_INET, SOCK_STREAM, 0) & 0) { myabort("Unable to create socket"); } #ifdef LINUX opt = 1; len = sizeof(opt); setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&opt,&len); #endif bzero((char *)&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_family = htons(MY_PORT); /* * The htonl (for a long integer) and htons (for short integer) convert * a host oriented byte order * into a network order. */ if (bind(sockfd,(struct sockaddr *)&servaddr,sizeof(struct sockaddr)) & 0) { myabort("Unable to bind socket"); } for (;;) { /* wait here */ n = recvfrom(sockfd, mesg, MAXM, 0, (struct sockaddr *)&clientInfo, sizeof(struct sockaddr)); doSomethingToIt(mesg); sendto(sockfd,mesg,n,0, (struct sockaddr *)&clientInfo, sizeof(struct sockaddr)); } } 看见了吗,处理每个消息只调用了两个函数,这比面向连接的协议更容易.但你必须,无论 如何,得在同一时间处理每个消息,因为消息从多台客户机向服务器涌来.而在面向连接的协 议中,子进程总是知道每条消息从哪里来. 客户机同样不能调用connect()系统调用.但是客户机可以直接调用sendto()函数.客户 机端和服务器端大致相同.只是它在调用recvfrom()之前调用sendto(): #include &sys/types.h& #include &sys/socket.h& int sendto((int sockfd, const void *message__, /* the pointer to message */ int length, /* of message */ unsigned int type, /* of routing, leave 0 */ const struct sockaddr * client, /* where to send it */ int length ); /* of sockaddr */ 注意:如果你使用的是BSD系统,请使用sendto()系统调用,不要使用sendmsg(),因为send to()性能更好. 如出错则返回-1,不过仅能检查出本地错误. recvfrom()系统调用是这样定义的: #include &sys/types.h& #include &sys/socket.h& int recvfrom(int sockfd, const void *message__, /* the pointer to message */ int length, /* of message */ unsigned int flags, /* of routing, leave 0 */ const struct sockaddr * client, /* where to send it */ int length ); /* of sockaddr */ 如果一个信息大得缓冲区都放不下,那么附加信息将被砍掉.该调用可以立即返回,也可 以永久的等待.这取决于你把flags设置成什么类型.你甚至可以设置超时(timeout)值.在说 明书(man pages)中可以找到recvfrom的更多信息. 在此你已学会了如何利用Linux的性能上的优点设计网络应用程序的基本知识.我们不打 算再进一步的描述更复杂的网络编程了.获得更多细节信息的一个极好的起点是参考W. Ric hard Stevens 的&&Unix 网络程序设计&&(Prentice Hall, 1990).此书乃众多大学所使用 的经典教材,内容极为详尽. 记录和文件锁定 当两个进程共享一个文件时,这之中存在一件非常危险的事情.如果一个进程改变了文件 目录那么必然影响到另一个进程.基于此理由,大多数操作系统采用一个互斥原则(mutually exclusive principle):当一个进程拥有一个文件时,其它进程就不能再碰这个文件.这叫做 文件锁定. 这个技术非常容易实现.通常所发生的事是,所谓"锁定文件"就是创建一个和源文件名同 名的文件再加上.lock扩展名.这就告诉其它进程这个文件不能再碰了.Linux假脱机打印系 统以及UUCP就是这样实现文件锁定的.这可能是一种粗暴的方法,但编程上非常简单. 不幸的是,你有几个进程要同时迅速的处理同一条信息时,这项技术对你并不实用.因为 等待文件打开和关闭所产生的延时将变得很长.同样一个进程如不能正确的释放文件,其它 进程将会挂在那里一直等待下去以获得存取权限. 由于这个原因,通常使用记录锁定.用记录锁定,一个大文件的一小部分被锁定以防止两 个进程同时改变它.如果有必要的话,记录锁定可以让多个进程同时存取相同文件的不同部 分记录.当然实现记录锁定编程要比实现文件锁定更复杂. 通常,要实现记录锁定你需要用到文件偏移量或是到文件起始处的字符数.在大多数程序 中,一个范围内的字符被锁定.程序记录锁定范围的起始处和长度,并保存在其它进程能查询 到的地方.不管是编写文件锁定还是记录锁定都需要对操作系统有很好的理解.但是并不难. 特别是可以从Internet,网络程序设计指导书和BBS上很容易地获得成千的程序.你可以察看 这些程序的源代码. 进程间通信 网络程序设计中通常包括两个或更多的进程将互相对话(interprocess communications ).因此进程通信的方法在网络程序设计中是极为重要的.网络程序设计在一些重要的方面不 同于一般程序设计通常所使用的方法.一个传统的程序可以通过全局变量或函数调用和不同 的模块(甚至同一机器上的其它应用程序)对话.但是在网络上却不行. 网络程序设计的一个重要的目标是保证进程间不互相干涉.否则系统可能被挂起或自锁. 因此,进程间必须使用简洁有效的方法进行通信.在此方面,UNIX具有非常显著的健壮性.因 为UNIX的许多基本性能如管道,队列等都非常适合网络. 和单个的应用程序代码相比,写进程间通信的代码十分复杂.如果你想写这类程序,可以 学习网络程序设计指导书和BBS站点上的例子程序.以了解这些任务是何以完成的. 小结 很少有人想写网络应用程序,因此进程的细节最好留给那些想写的人.实践和查阅大量的 例子程序是开始写网络代码的最好的方法.但是要掌握这门技术却要花许多年时间.
&— zwell @ 2:27 am
网络安全编程技术(一)
UNIX系统为程序员提供了许多子程序,这些子程序可存取各种安全属性.有& 些是信息子程序,返回文件属性,实际的和有效的UID,GID等信息.有些子程序可& 改变文件属性.UID,GID等有些处理口令文件和小组文件,还有些完成加密和解密.& 本文主要讨论有关系统子程序,标准C库子程序的安全,如何写安全的C程序& 并从root的角度介绍程序设计(仅能被root调用的子程序).
1.系统子程序
(1)I/O子程序& *creat():建立一个新文件或重写一个暂存文件.& 需要两个参数:文件名和存取许可值(8进制方式).如:& creat(“/usr/pat/read_write”,0666) /* 建立存取许可方式为0666的文件 */& 调用此子程序的进程必须要有建立的文件的所在目录的写和执行许可,置& 给creat()的许可方式变量将被umask()设置的文件建立屏蔽值所修改,新& 文件的所有者和小组由有效的UID和GID决定.& 返回值为新建文件的文件描述符.& *fstat():见后面的stat().& *open():在C程序内部打开文件.& 需要两个参数:文件路径名和打开方式(I,O,I&O).& 如果调用此子程序的进程没有对于要打开的文件的正确存取许可(包括文& 件路径上所有目录分量的搜索许可),将会引起执行失败.& 如果此子程序被调用去打开不存在的文件,除非设置了O_CREAT标志,调用& 将不成功.此时,新文件的存取许可作为第三个参数(可被用户的umask修& 改).& 当文件被进程打开后再改变该文件或该文件所在目录的存取许可,不影响& 对该文件的I/O操作.& *read():从已由open()打开并用作输入的文件中读信息.& 它并不关心该文件的存取许可.一旦文件作为输入打开,即可从该文件中读& 取信息.& *write():输出信息到已由open()打开并用作输出的文件中.同read()一样& 它也不关心该文件的存取许可.& (2)进程控制& *exec()族:包括execl(),execv(),execle(),execve(),execlp()和execvp()& 可将一可执行模快拷贝到调用进程占有的存贮空间.正被调用进& 程执行的程序将不复存在,新程序取代其位置.& 这是UNIX系统中一个程序被执行的唯一方式:用将执行的程序复盖原有的& 程序.& 安全注意事项:& . 实际的和有效的UID和GID传递给由exec()调入的不具有SUID和SGID许& 可的程序.& . 如果由exec()调入的程序有SUID和SGID许可,则有效的UID和GID将设& 置给该程序的所有者或小组.& . 文件建立屏蔽值将传递给新程序.& . 除设了对exec()关闭标志的文件外,所有打开的文件都传递给新程序.& 用fcntl()子程序可设置对exec()的关闭标志.& *fork():用来建立新进程.其建立的子进程是与调用fork()的进程(父进程)& 完全相同的拷贝(除了进程号外)& 安全注意事项:& . 子进程将继承父进程的实际和有效的UID和GID.& . 子进程继承文件方式建立屏蔽值.& . 所有打开的文件传给子进程.& *signal():允许进程处理可能发生的意外事件和中断.& 需要两个参数:信号编号和信号发生时要调用的子程序.& 信号编号定义在signal.h中.& 信号发生时要调用的子程序可由用户编写,也可用系统给的值,如:SIG_IGN& 则信号将被忽略,SIG_DFL则信号将按系统的缺省方式处理.& 如许多与安全有关的程序禁止终端发中断信息(BREAK和DELETE),以免自己& 被用户终端终止运行.& 有些信号使UNIX系统的产生进程的核心转储(进程接收到信号时所占内存& 的内容,有时含有重要信息),此系统子程序可用于禁止核心转储.& (3)文件属性& *access():检测指定文件的存取能力是否符合指定的存取类型.& 需要两个参数:文件名和要检测的存取类型(整数).& 下面还有喔 (19%) │ 结束 ← q │ ↑/↓/PgUp/PgDn 移动 │ ? 辅助说明 │& 需要两个参数:文件名和要检测的存取类型(整数).& 存取类型定义如下:& 0: 检查文件是否存在& 1: 检查是否可执行(搜索)& 2: 检查是否可写& 3: 检查是否可写和执行
4: 检查是否可读& 5: 检查是否可读和执行& 6: 检查是否可读可写可执行& 这些数字的意义和chmod命令中规定许可方式的数字意义相同.& 此子程序使用实际的UID和GID检测文件的存取能力(一般有效的UID和GID& 用于检查文件存取能力).& 返回值: 0:许可 -1:不许可.& *chmod():将指定文件或目录的存取许可方式改成新的许可方式.& 需要两个参数:文件名和新的存取许可方式.& *chown():同时改变指定文件的所有者和小组的UID和GID.(与chown命令不& 同).& 由于此子程序同时改变文件的所有者和小组,故必须取消所操作文件的SUID& 和SGID许可,以防止用户建立SUID和SGID程序,然后运行chown()去获得别& 人的权限.& *stat():返回文件的状态(属性).& 需要两个参数:文件路径名和一个结构指针,指向状态信息的存放& 的位置.& 结构定义如下:& st_mode: 文件类型和存取许可方式& st_ino: I节点号& st_dev: 文件所在设备的ID& st_rdev: 特别文件的ID& st_nlink: 文件链接数& st_uid: 文件所有者的UID& st_gid: 文件小组的GID& st_size: 按字节计数的文件大小& st_atime: 最后存取时间(读)& st_mtime: 最后修改时间(写)和最后状态的改变& st_ctime: 最后的状态修改时间& 返回值: 0:成功 1:失败& *umask():将调用进程及其子进程的文件建立屏蔽值设置为指定的存取许可.& 需要一个参数: 新的文件建立屏值.& (4)UID和GID的处理& *getuid():返回进程的实际UID.& *getgid():返回进程的实际GID.& 以上两个子程序可用于确定是谁在运行进程.& *geteuid():返回进程的有效UID.& *getegid():返回进程的有效GID.& 以上两个子程序可在一个程序不得不确定它是否在运行某用户而不是运行& 它的用户的SUID程序时很有用,可调用它们来检查确认本程序的确是以该& 用户的SUID许可在运行.& *setuid():用于改变有效的UID.& 对于一般用户,此子程序仅对要在有效和实际的UID之间变换的SUID程序才& 有用(从原有效UID变换为实际UID),以保护进程不受到安全危害.实际上该& 进程不再是SUID方式运行.& *setgid():用于改变有效的GID.
网络安全编程技术(二)
(1)标准I/O& *fopen():打开一个文件供读或写,安全方面的考虑同open()一样.& *fread(),getc(),fgetc(),gets(),scanf()和fscanf():从已由fopen()打& 开供读的文件中读取信息.它们并不关心文件的存取许可.这一点& 同read().& *fwrite(),put(),fputc(),puts,fputs(),printf(),fprintf():写信息到& 已由fopen()打开供写的文件中.它们也不关心文件的存取许可.& 同write().& *getpass():从终端上读至多8个字符长的口令,不回显用户输入的字符.& 需要一个参数: 提示信息.& 该子程序将提示信息显示在终端上,禁止字符回显功能,从/dev/tty读取口& 令,然后再恢复字符回显功能,返回刚敲入的口令的指针.& *popen():将在(5)运行shell中介绍.
(2)/etc/passwd处理& 有一组子程序可对/etc/passwd文件进行方便的存取,可对文件读取到入口& 项或写新的入口项或更新等等.& *getpwuid():从/etc/passwd文件中获取指定的UID的入口项.& *getpwnam():对于指定的登录名,在/etc/passwd文件检索入口项.& 以上两个子程序返回一指向passwd结构的指针,该结构定义在& /usr/include/pwd.h中,定义如下:& struct passwd {& char * pw_ /* 登录名 */& char * pw_ /* 加密后的口令 */& uid_t pw_ /* UID */& gid_t pw_ /* GID */& char * pw_ /* 代理信息 */& char * pw_ /* 注释 */& char * pw_& char * pw_ /* 主目录 */& char * pw_ /* 使用的shell */& };& *getpwent(),setpwent(),endpwent():对口令文件作后续处理.& 状蔚饔胓etpwent(),打开/etc/passwd并返回指向文件中第一个入口项的& 指针,保持调用之间文件的打开状态.& 再调用getpwent()可顺序地返回口令文件中的各入口项.& 调用setpwent()把口令文件的指针重新置为文件的开始处.& 使用完口令文件后调用endpwent()关闭口令文件.& *putpwent():修改或增加/etc/passwd文件中的入口项.& 此子程序将入口项写到一个指定的文件中,一般是一个临时文件,直接写口& 令文件是很危险的.最好在执行前做文件封锁,使两个程序不能同时写一个& 文件.算法如下:& . 建立一个独立的临时文件,即/etc/passnnn,nnn是PID号.& . 建立新产生的临时文件和标准临时文件/etc/ptmp的链,若建链失败,& 则为有人正在使用/etc/ptmp,等待直到/etc/ptmp可用为止或退出.& . 将/etc/passwd拷贝到/etc/ptmp,可对此文件做任何修改.& . 将/etc/passwd移到备份文件/etc/opasswd.& . 建立/etc/ptmp和/etc/passwd的链.
. 断开/etc/passnnn与/etc/ptmp的链.& 注意:临时文件应建立在/etc目录,才能保证文件处于同一文件系统中,建& 链才能成功,且临时文件不会不安全.此外,若新文件已存在,即便建& 链的是root用户,也将失败,从而保证了一旦临时文件成功地建链后& 没有人能再插进来干扰.当然,使用临时文件的程序应确保清除所有& 临时文件,正确地捕捉信号.& (3)/etc/group的处理& 有一组类似于前面的子程序处理/etc/group的信息,使用时必须用include& 语句将/usr/include/grp.h文件加入到自己的程序中.该文件定义了group& 结构,将由getgrnam(),getgrgid(),getgrent()返回group结构指针.& *getgrnam():在/etc/group文件中搜索指定的小组名,然后返回指向小组入& 口项的指针.& *getgrgid():类似于前一子程序,不同的是搜索指定的GID.& *getgrent():返回group文件中的下一个入口项.& *setgrent():将group文件的文件指针恢复到文件的起点.& *endgrent():用于完成工作后,关闭group文件.& *getuid():返回调用进程的实际UID.& *getpruid():以getuid()返回的实际UID为参数,确定与实际UID相应的登录& 名,或指定一UID为参数.& *getlogin():返回在终端上登录的用户的指针.& 系统依次检查STDIN,STDOUT,STDERR是否与终端相联,与终端相联的标准输& 入用于确定终端名,终端名用于查找列于/etc/utmp文件中的用户,该文件& 由login维护,由who程序用来确认用户.& *cuserid():首先调用getlogin(),若getlogin()返回NULL指针,再调用& getpwuid(getuid()).& *以下为命令:& *logname:列出登录进终端的用户名.& *who am i:显示出运行这条命令的用户的登录名.& *id:显示实际的UID和GID(若有效的UID和GID和实际的不同时也显示有效的& UID和GID)和相应的登录名.
(4)加密子程序& 1977年1月,NBS宣布一个用于美国联邦政府ADP系统的网络的标准加密法:数& 据加密标准即DES用于非机密应用方面.DES一次处理64BITS的块,56位的加& 密键.& *setkey(),encrypt():提供用户对DES的存取.& 此两子程序都取64BITS长的字符数组,数组中的每个元素代表一个位,为0& 或1.setkey()设置将按DES处理的加密键,忽略每第8位构成一个56位的加& 密键.encrypt()然后加密或解密给定的64BITS长的一块,加密或解密取决& 于该子程序的第二个变元,0:加密 1:解密.& *crypt():是UNIX系统中的口令加密程序,也被/usr/lib/makekey命令调用.& crypt()子程序与crypt命令无关,它与/usr/lib/makekey一样取8个字符长& 的关键词,2个salt字符.关键词送给setkey(),salt字符用于混合encrypt()& 中的DES算法,最终调用encrypt()重复25次加密一个相同的字符串.& 返回加密后的字符串指针.
(5)运行shell& *system():运行/bin/sh执行其参数指定的命令,当指定命令完成时返回.& *popen():类似于system(),不同的是命令运行时,其标准输入或输出联到由& popen()返回的文件指针.& 二者都调用fork(),exec(),popen()还调用pipe(),完成各自的工作,因而& fork()和exec()的安全方面的考虑开始起作用.
网络安全编程技术(三)
3.写安全的C程序& 一般有两方面的安全问题,在写程序时必须考虑:& (1)确保自己建立的任何临时文件不含有机密数据,如果有机密数据,设置& 临时文件仅对自己可读/写.确保建立临时文件的目录仅对自己可写.& (2)确保自己要运行的任何命令(通过system(),popen(),execlp(),& execvp()运行的命令)的确是自己要运行的命令,而不是其它什么命& 令,尤其是自己的程序为SUID或SGID许可时要小心.& 第一方面比较简单,在程序开始前调用umask(077).若要使文件对其他人可& 读,可再调chmod(),也可用下述语名建立一个”不可见”的临时文件.& creat(“/tmp/xxx”,0);& file=open(“/tmp/xxx”,O_RDWR);& unlink(“/tmp/xxx”);& 文件/tmp/xxx建立后,打开,然后断开链,但是分配给该文件的存储器并未删& 除,直到最终指向该文件的文件通道被关闭时才被删除.打开该文件的进程& 和它的任何子进程都可存取这个临时文件,而其它进程不能存取该文件,因& 为它在/tmp中的目录项已被unlink()删除.& 第二方面比较复杂而微妙,由于system(),popen(),execlp(),execvp()执行& 时,若不给出执行命令的全路径,就能”骗”用户的程序去执行不同的命令.因& 为系统子程序是根据PATH变量确定哪种顺序搜索哪些目录,以寻找指定的命& 令,这称为SUID陷井.最安全的办法是在调用system()前将有效UID改变成实& 际UID,另一种比较好的方法是以全路径名命令作为参数.execl(),execv(),& execle(),execve()都要求全路径名作为参数.有关SUID陷井的另一方式是& 在程序中设置PATH,由于system()和popen()都启动shell,故可使用shell句& 法.如:& system(“PATH=/bin:/usr/bin cd”);& 这样允许用户运行系统命令而不必知道要执行的命令在哪个目录中,但这种& 方法不能用于execlp(),execvp()中,因为它们不能启动shell执行调用序列& 传递的命令字符串.& 关于shell解释传递给system()和popen()的命令行的方式,有两个其它的问& 题:& *shell使用IFS shell变量中的字符,将命令行分解成单词(通常这个& shell变量中是空格,tab,换行),如IFS中是/,字符串/bin/ed被解释成单词& bin,接下来是单词ed,从而引起命令行的曲解.
再强调一次:在通过自己的程序运行另一个程序前,应将有效UID改为实际的& UID,等另一个程序退出后,再将有效UID改回原来的有效UID.
SUID/SGID程序指导准则& (1)不要写SUID/SGID程序,大多数时候无此必要.& (2)设置SGID许可,不要设置SUID许可.应独自建立一个新的小组.& (3)不要用exec()执行任何程序.记住exec()也被system()和popen()调用.& . 若要调用exec()(或system(),popen()),应事先用setgid(getgid())& 将有效GID置加实际GID.& . 若不能用setgid(),则调用system()或popen()时,应设置IFS:& popen(“IFS= ;export IFS;/bin/ls”,”r”);& . 使用要执行的命令的全路径名.& . 若不能使用全路径名,则应在命令前先设置PATH:& popen(“IFS= ;export IFS;PATH=/bin:/usr//bin/ls”,”r”);& . 不要将用户规定的参数传给system()或popen();若无法避免则应检查& 变元字符串中是否有特殊的shell字符.& . 若用户有个大程序,调用exec()执行许多其它程序,这种情况下不要将& 大程序设置为SGID许可.可以写一个(或多个)更小,更简单的SGID程序& 执行必须具有SGID许可的任务,然后由大程序执行这些小SGID程序.& (4)若用户必须使用SUID而不是SGID,以相同的顺序记住(2),(3)项内容,并& 相应调整.不要设置root的SUID许可.选一个其它户头.& (5)若用户想给予其他人执行自己的shell程序的许可,但又不想让他们能& 读该程序,可将程序设置为仅执行许可,并只能通过自己的shell程序来& 运行.& 编译,安装SUID/SGID程序时应按下面的方法& (1)确保所有的SUID(SGID)程序是对于小组和其他用户都是不可写的,存取& 权限的限制低于)将带来麻烦.只能更严格.)将使& 其他人无法寻找程序中的安全漏洞.& (2)警惕外来的编码和make/install方法& . 某些make/install方法不加选择地建立SUID/SGID程序.& . 检查违背上述指导原则的SUID/SGID许可的编码.& . 检查makefile文件中可能建立SUID/SGID文件的命令.& 4.root程序的设计& 有若干个子程序可以从有效UID为0的进程中调用.许多前面提到的子程序,& 当从root进程中调用时,将完成和原来不同的处理.主要是忽略了许可权限的检& 查.& 由root用户运行的程序当然是root进程(SUID除外),因有效UID用于确定文& 件的存取权限,所以从具有root的程序中,调用fork()产生的进程,也是root进程.
(1)setuid():从root进程调用setuid()时,其处理有所不同,setuid()将把有& 效的和实际的UID都置为指定的值.这个值可以是任何整型数.而对非root& 进程则仅能以实际UID或本进程原来有效的UID为变量值调用setuid().& (2)setgid():在系统进程中调用setgid()时,与setuid()类似,将实际和有效& 的GID都改变成其参数指定的值.& * 调用以上两个子程序时,应当注意下面几点:& . 调用一次setuid()(setgid())将同时设置有效和实际UID(GID),独立分& 别设置有效或实际UID(GID)固然很好,但无法做到这点.& . setuid()(setgid())可将有效和实际UID(GID)设置成任何整型数,其数& 值不必一定与/etc/passwd(/etc/group)中用户(小组)相关联.& . 一旦程序以一个用户的UID了setuid(),该程序就不再做为root运行,也& 不可能再获root特权.& (3)chown():当root进程运行chown()时,chown()将不删除文件的SUID和/或& SGID许可,但当非root进程运行chown()时,chown()将取消文件的SUID和/& 或SGID许可.& (4)chroot():改变进程对根目录的概念,调用chroot()后,进程就不能把当前& 工作目录改变到新的根目录以上的任一目录,所有以/开始的路径搜索,都& 从新的根目录开始.& (5)mknod():用于建立一个文件,类似于creat(),差别是mknod()不返回所打开& 文件的文件描述符,并且能建立任何类型的文件(普通文件,特殊文件,目录& 文件).若从非root进程调用mknod()将执行失败,只有建立FIFO特别文件& (有名管道文件)时例外,其它任何情况下,必须从root进程调用mknod().由& 于creat()仅能建立普通文件,mknod()是建立目录文件的唯一途径,因而仅& 有root能建立目录,这就是为什么mkdir命令具有SUID许可并属root所有.& 一般不从程序中调用mknod().通常用/etc/mknod命令建立特别设备文件而& 这些文件一般不能在使用着时建立和删除,mkdir命令用于建立目录.当用& mknod()建立特别文件时,应当注意确从所建的特别文件不允许存取内存,& 磁盘,终端和其它设备.& (6)unlink():用于删除文件.参数是要删除文件的路径名指针.当指定了目录& 时,必须从root进程调用unlink(),这是必须从root进程调用unlink()的唯& 一情况,这就是为什么rmdir命令具有root的SGID许可的原因.& (7)mount(),umount():由root进程调用,分别用于安装和拆卸文件系统.这两& 个子程序也被mount和umount命令调用,其参数基本和命令的参数相同.调& 用mount(),需要给出一个特别文件和一个目录的指针,特别文件上的文件& 系统就将安装在该目录下,调用时还要给出一个标识选项,指定被安装的文& 件系统要被读/写(0)还是仅读(1).umount()的参数是要一个要拆卸的特别& 文件的指针.
&— zwell @ 7:14 pm
Linux系统的一个主要特点是他的网络功能非常强大。随着网络的日益普及,基于网络的应用也将越来越多。 在这个网络时代,掌握了Linux的网络编程技术,将令每一个人处于不败之地,学习Linux的网络编程,可以让我们真正的体会到网络的魅力。 想成为一位真正的hacker,必须掌握网络编程技术。& 现在书店里面已经有了许多关于Linux网络编程方面的书籍,网络上也有了许多关于网络编程方面的教材,大家都可以 去看一看的。在这里我会和大家一起来领会Linux网络编程的奥妙,由于我学习Linux的网络编程也开始不久,所以我下面所说的肯定会有错误的, 还请大家指点出来,在这里我先谢谢大家了。
在这一个章节里面,我会和以前的几个章节不同,在前面我都是概括的说了一下, 从现在开始我会尽可能的详细的说明每一个函数及其用法。好了让我们去领会Linux的伟大的魅力吧!
1. Linux网络知识介绍
1.1 客户端程序和服务端程序& 网络程序和普通的程序有一个最大的区别是网络程序是由两个部分组成的–客户端和服务器端.
网络程序是先有服务器程序启动,等待客户端的程序运行并建立连接.一般的来说是服务端的程序 在一个端口上监听,直到有一个客户端的程序发来了请求.
1.2 常用的命令& 由于网络程序是有两个部分组成,所以在调试的时候比较麻烦,为此我们有必要知道一些常用的网络命令
netstat& 命令netstat是用来显示网络的连接,路由表和接口统计等网络的信息.netstat有许多的选项 我们常用的选项是 -an 用来显示详细的网络状态.至于其它的选项我们可以使用帮助手册获得详细的情况.
telnet& telnet是一个用来远程控制的程序,但是我们完全可以用这个程序来调试我们的服务端程序的. 比如我们的服务器程序在监听8888端口,我们可以用telnet localhost 8888来查看服务端的状况.
1.3 TCP/UDP介绍& TCP(Transfer Control Protocol)传输控制协议是一种面向连接的协议,当我们的网络程序使用 这个协议的时候,网络可以保证我们的客户端和服务端的连接是可靠的,安全的.
UDP(User Datagram Protocol)用户数据报协议是一种非面向连接的协议,这种协议并不能保证我们 的网络程序的连接是可靠的,所以我们现在编写的程序一般是采用TCP协议的.
2. 初等网络函数介绍(TCP)& Linux系统是通过提供套接字(socket)来进行网络编程的.网络程序通过socket和其它几个函数的调用,会返回一个 通讯的文件描述符,我们可以将这个描述符看成普通的文件的描述符来操作,这就是linux的设备无关性的 好处.我们可以通过向描述符读写操作实现网络之间的数据交流.
2.1 socket& int socket(int domain, int type,int protocol)
domain:说明我们网络程序所在的主机采用的通讯协族(AF_UNIX和AF_INET等). AF_UNIX只能够用于单一的Unix系统进程间通信,而AF_INET是针对Internet的,因而可以允许在远程 主机之间通信(当我们 man socket时发现 domain可选项是 PF_*而不是AF_*,因为glibc是posix的实现 所以用PF代替了AF,不过我们都可以使用的).
type:我们网络程序所采用的通讯协议(SOCK_STREAM,SOCK_DGRAM等) SOCK_STREAM表明我们用的是TCP协议,这样会提供按顺序的,可靠,双向,面向连接的比特流. SOCK_DGRAM 表明我们用的是UDP协议,这样只会提供定长的,不可靠,无连接的通信.
protocol:由于我们指定了type,所以这个地方我们一般只要用0来代替就可以了 socket为网络通讯做基本的准备.成功时返回文件描述符,失败时返回-1,看errno可知道出错的详细情况.
2.2 bind& int bind(int sockfd, struct sockaddr *my_addr, int addrlen)
sockfd:是由socket调用返回的文件描述符.
addrlen:是sockaddr结构的长度.
my_addr:是一个指向sockaddr的指针. 在中有 sockaddr的定义
struct sockaddr{& unisgned short as_& char sa_data[14];& };
不过由于系统的兼容性,我们一般不用这个头文件,而使用另外一个结构(struct sockaddr_in) 来代替.在中有sockaddr_in的定义& struct sockaddr_in{& unsigned short sin_& unsigned short int sin_& struct in_addr sin_& unsigned char sin_zero[8];
我们主要使用Internet所以sin_family一般为AF_INET,sin_addr设置为INADDR_ANY表示可以 和任何的主机通信,sin_port是我们要监听的端口号.sin_zero[8]是用来填充的. Bind将本地的端口同socket返回的文件描述符捆绑在一起.成功是返回0,失败的情况和socket一样
2.3 listen& int listen(int sockfd,int backlog)
sockfd:是bind后的文件描述符.
backlog:设置请求排队的最大长度.当有多个客户端程序和服务端相连时, 使用这个表示可以介绍的排队长度. Listen函数将bind的文件描述符变为监听套接字.返回的情况和bind一样.
2.4 accept& int accept(int sockfd, struct sockaddr *addr,int *addrlen)
sockfd:是listen后的文件描述符.
addr,addrlen是用来给客户端的程序填写的,服务器端只要传递指针就可以了. Bind,listen和accept是服务器端用的函数,accept调用时,服务器端的程序会一直阻塞到有一个 客户程序发出了连接. Accept成功时返回最后的服务器端的文件描述符,这个时候服务器端可以向该描述符写信息了. 失败时返回-1
2.5 connect& int connect(int sockfd, struct sockaddr * serv_addr,int addrlen)
sockfd:socket返回的文件描述符.
serv_addr:储存了服务器端的连接信息.其中sin_add是服务端的地址
addrlen:serv_addr的长度
connect函数是客户端用来同服务端连接的.成功时返回0,sockfd是同服务端通讯的文件描述符 失败时返回-1.
服务器端程序
/******* 服务器程序 (server.c) ************/& #include& #include& #include& #include& #include& #include& #include& #include
int main(int argc, char *argv[])& {& int sockfd,new_& struct sockaddr_in server_& struct sockaddr_in client_& int sin_size,& char hello[]=”Hello! Are You Fine?\n”;
if(argc!=2)& {& fprintf(stderr,”Usage:%s portnumber\a\n”,argv[0]);& exit(1);& }
if((portnumber=atoi(argv[1]))&0)& {& fprintf(stderr,”Usage:%s portnumber\a\n”,argv[0]);& exit(1);& }
/* 服务器端开始建立socket描述符 */& if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)& {& fprintf(stderr,”Socket error:%s\n\a”,strerror(errno));& exit(1);& }
/* 服务器端填充 sockaddr结构 */& bzero(&server_addr,sizeof(struct sockaddr_in));& server_addr.sin_family=AF_INET;& server_addr.sin_addr.s_addr=htonl(INADDR_ANY);& server_addr.sin_port=htons(portnumber);
/* 捆绑sockfd描述符 */& if(bind(sockfd,(struct sockaddr *)(&server_addr),sizeof(struct sockaddr))==-1)& {& fprintf(stderr,”Bind error:%s\n\a”,strerror(errno));& exit(1);& }
/* 监听sockfd描述符 */& if(listen(sockfd,5)==-1)& {& fprintf(stderr,”Listen error:%s\n\a”,strerror(errno));& exit(1);& }
while(1)& {& /* 服务器阻塞,直到客户程序建立连接 */& sin_size=sizeof(struct sockaddr_in);& if((new_fd=accept(sockfd,(struct sockaddr *)(&client_addr),&sin_size))==-1)& {& fprintf(stderr,”Accept error:%s\n\a”,strerror(errno));& exit(1);& }
fprintf(stderr,”Server get connection from %s\n”,& inet_ntoa(client_addr.sin_addr));& if(write(new_fd,hello,strlen(hello))==-1)& {& fprintf(stderr,”Write Error:%s\n”,strerror(errno));& exit(1);& }& /* 这个通讯已经结束 */& close(new_fd);& /* 循环下一个 */& }& close(sockfd);& exit(0);& }
客户端程序
/******* 客户端程序 client.c ************/& #include& #include& #include& #include& #include& #include& #include& #include
int main(int argc, char *argv[])& {&& char buffer[1024];& struct sockaddr_in server_& struct hostent *& int portnumber,
if(argc!=3)& {& fprintf(stderr,”Usage:%s hostname portnumber\a\n”,argv[0]);& exit(1);& }
if((host=gethostbyname(argv[1]))==NULL)& {& fprintf(stderr,”Gethostname error\n”);& exit(1);& }
if((portnumber=atoi(argv[2]))&0)& {& fprintf(stderr,”Usage:%s hostname portnumber\a\n”,argv[0]);& exit(1);& }
/* 客户程序开始建立 sockfd描述符 */& if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)& {& fprintf(stderr,”Socket Error:%s\a\n”,strerror(errno));& exit(1);& }
/* 客户程序填充服务端的资料 */& bzero(&server_addr,sizeof(server_addr));& server_addr.sin_family=AF_INET;& server_addr.sin_port=htons(portnumber);& server_addr.sin_addr=*((struct in_addr *)host-&h_addr);
/* 客户程序发起连接请求 */& if(connect(sockfd,(struct sockaddr *)(&server_addr),sizeof(struct sockaddr))==-1)& {& fprintf(stderr,”Connect Error:%s\a\n”,strerror(errno));& exit(1);& }
/* 连接成功了 */& if((nbytes=read(sockfd,buffer,1024))==-1)& {& fprintf(stderr,”Read Error:%s\n”,strerror(errno));& exit(1);& }& buffer[nbytes]=\;& printf(“I have received:%s\n”,buffer);& /* 结束通讯 */& close(sockfd);& exit(0);& }
MakeFile& 这里我们使用GNU 的make实用程序来编译. 关于make的详细说明见 Make 使用介绍
######### Makefile ###########& all:server client& server:server.c& gcc $^ -o $@& client:client.c& gcc $^ -o $@
运行make后会产生两个程序server(服务器端)和client(客户端) 先运行./server portnumber& (portnumber随便取一个大于1204且不在/etc/services中出现的号码 就用8888好了),然后运行 ./client localhost 8888 看看有什么结果. (你也可以用telnet和netstat试一试.) 上面是一个最简单的网络程序,不过是不是也有点烦.上面有许多函数我们还没有解释. 我会在下一章进行的详细的说明.
2.7 总结& 总的来说网络程序是由两个部分组成的–客户端和服务器端.它们的建立步骤一般是:
服务器端& socket–&bind–&listen–&accept
客户端& socket–&connect
3. 服务器和客户机的信息函数& 这一章我们来学习转换和网络方面的信息函数.
3.1 字节转换函数& 在网络上面有着许多类型的机器,这些机器在表示数据的字节顺序是不同的, 比如i386芯片是低字节在内存地址的低端,高字节在高端,而alpha芯片却相反. 为了统一起来,在Linux下面,有专门的字节转换函数.
unsigned long int htonl(unsigned long int hostlong)& unsigned short int htons(unisgned short int hostshort)& unsigned long int ntohl(unsigned long int netlong)& unsigned short int ntohs(unsigned short int netshort)
在这四个转换函数中,h 代表host, n 代表 network.s 代表short l 代表long 第一个函数的意义是将本机器上的long数据转化为网络上的long. 其他几个函数的意义也差不多.
3.2 IP和域名的转换& 在网络上标志一台机器可以用IP或者是用域名.那么我们怎么去进行转换呢?
struct hostent *gethostbyname(const char *hostname)& struct hostent *gethostbyaddr(const char *addr,int len,int type)& 在中有struct hostent的定义& struct hostent{& char *h_ /* 主机的正式名称 */& char *h_ /* 主机的别名 */& int h_ /* 主机的地址类型 AF_INET*/& int h_ /* 主机的地址长度 对于IP4 是4字节32位*/& char **h_addr_ /* 主机的IP地址列表 */& }& #define h_addr h_addr_list[0] /* 主机的第一个IP地址*/
gethostbyname可以将机器名(如 linux.yessun.com)转换为一个结构指针.在这个结构里面储存了域名的信息& gethostbyaddr可以将一个32位的IP地址(C0A80001)转换为结构指针.
这两个函数失败时返回NULL 且设置h_errno错误变量,调用h_strerror()可以得到详细的出错信息
3.3 字符串的IP和32位的IP转换.& 在网络上面我们用的IP都是数字加点(192.168.0.1)构成的, 而在struct in_addr结构中用的是32位的IP, 我们上面那个32位IP(C0A80001)是的192.168.0.1 为了转换我们可以使用下面两个函数
int inet_aton(const char *cp,struct in_addr *inp)& char *inet_ntoa(struct in_addr in)
函数里面 a 代表 ascii n 代表network.第一个函数表示将a.b.c.d的IP转换为32位的IP,存储在 inp指针里面.第二个是将32位IP转换为a.b.c.d的格式.
3.4 服务信息函数& 在网络程序里面我们有时候需要知道端口.IP和服务信息.这个时候我们可以使用以下几个函数
int getsockname(int sockfd,struct sockaddr *localaddr,int *addrlen)& int getpeername(int sockfd,struct sockaddr *peeraddr, int *addrlen)& struct servent *getservbyname(const char *servname,const char *protoname)& struct servent *getservbyport(int port,const char *protoname)& struct servent& {& char *s_ /* 正式服务名 */& char **s_ /* 别名列表 */& int s_ /* 端口号 */& char *s_ /* 使用的协议 */& }
一般我们很少用这几个函数.对应客户端,当我们要得到连接的端口号时在connect调用成功后使用可得到 系统分配的端口号.对于服务端,我们用INADDR_ANY填充后,为了得到连接的IP我们可以在accept调用成功后 使用而得到IP地址.
在网络上有许多的默认端口和服务,比如端口21对ftp80对应WWW.为了得到指定的端口号的服务 我们可以调用第四个函数,相反为了得到端口号可以调用第三个函数.
3.5 一个例子
#include& #include& #include& #include& #include
int main(int argc ,char **argv)& {& struct sockaddr_& struct hostent *& char **
if(argc&2)& {& fprintf(stderr,”Usage:%s hostname|ip..\n\a”,argv[0]);& exit(1);& }
argv++;& for(;*argv!=NULL;argv++)& {& /* 这里我们假设是IP*/& if(inet_aton(*argv,&addr.sin_addr)!=0)& {& host=gethostbyaddr((char *)&addr.sin_addr,4,AF_INET);& printf(“Address information of Ip %s\n”,*argv);& }& else& {& /* 失败,难道是域名?*/& host=gethostbyname(*argv); printf(“Address information& of host %s\n”,*argv);& }& if(host==NULL)& {& /* 都不是 ,算了不找了*/& fprintf(stderr,”No address information of %s\n”,*argv);&& }& printf(“Official host name %s\n”,host-&h_name);& printf(“Name aliases:”);& for(alias=host-&h_*alias!=NULL;alias++)& printf(“%s ,”,*alias);& printf(“\nIp address:”);& for(alias=host-&h_addr_*alias!=NULL;alias++)& printf(“%s ,”,inet_ntoa(*(struct in_addr *)(*alias)));& }& }
在这个例子里面,为了判断用户输入的是IP还是域名我们调用了两个函数,第一次我们假设输入的是IP所以调用inet_aton, 失败的时候,再调用gethostbyname而得到信息.
4. 完整的读写函数& 一旦我们建立了连接,我们的下一步就是进行通信了.在Linux下面把我们前面建立的通道 看成是文件描述符,这样服务器端和客户端进行通信时候,只要往文件描述符里面读写东西了. 就象我们往文件读写一样.
4.1 写函数write
ssize_t write(int fd,const void *buf,size_t nbytes)
write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节数.失败时返回-1. 并设置errno变量. 在网络程序中,当我们向套接字文件描述符写时有俩种可能.
1)write的返回值大于0,表示写了部分或者是全部的数据.
2)返回的值小于0,此时出现了错误.我们要根据错误类型来处理.
如果错误为EINTR表示在写的时候出现了中断错误.
如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接).
为了处理以上的情况,我们自己编写一个写函数来处理这几种情况.
int my_write(int fd,void *buffer,int length)& {& int bytes_& int written_& char *
ptr=& bytes_left=& while(bytes_left&0)& {& /* 开始写*/& written_bytes=write(fd,ptr,bytes_left);& if(written_bytes&=0) /* 出错了*/& {& if(errno==EINTR) /* 中断错误 我们继续写*/& written_bytes=0;& else /* 其他错误 没有办法,只好撤退了*/& return(-1);& }& bytes_left-=written_& ptr+=written_ /* 从剩下的地方继续写 */& }& return(0);& }
4.2 读函数read& ssize_t read(int fd,void *buf,size_t nbyte) read函数是负责从fd中读取内容.当读成功时,read返回实际所读的字节数,如果返回的值是0 表示已经读到文件的结束了,小于0表示出现了错误.如果错误为EINTR说明读是由中断引起的, 如果是ECONNREST表示网络连接出了问题. 和上面一样,我们也写一个自己的读函数.
int my_read(int fd,void *buffer,int length)& {& int bytes_& int bytes_& char *
bytes_left=& while(bytes_left&0)& {& bytes_read=read(fd,ptr,bytes_read);& if(bytes_read&0)& {& if(errno==EINTR)& bytes_read=0;& else& return(-1);& }& else if(bytes_read==0)&& bytes_left-=bytes_& ptr+=bytes_& }& return(length-bytes_left);& }
4.3 数据的传递& 有了上面的两个函数,我们就可以向客户端或者是服务端传递数据了.比如我们要传递一个结构.可以使用如下方式
/* 客户端向服务端写 */
struct my_struct my_struct_& write(fd,(void *)&my_struct_client,sizeof(struct my_struct);
/* 服务端的读*/& char buffer[sizeof(struct my_struct)];& struct *my_struct_& read(fd,(void *)buffer,sizeof(struct my_struct));& my_struct_server=(struct my_struct *)
在网络上传递数据时我们一般都是把数据转化为char类型的数据传递.接收的时候也是一样的 注意的是我们没有必要在网络上传递指针(因为传递指针是没有任何意义的,我们必须传递指针所指向的内容)
5. 用户数据报发送& 我们前面已经学习网络程序的一个很大的部分,由这个部分的知识,我们实际上可以写出大部分的基于TCP协议的网络程序了.现在在Linux下的大部分程序都是用我们上面所学的知识来写的.我们可以去找一些源程序来参考一下.这一章,我们简单的学习一下基于UDP协议的网络程序.
5.1 两个常用的函数
int recvfrom(int sockfd,void *buf,int len,unsigned int flags,struct sockaddr * from int *fromlen)& int sendto(int sockfd,const void *msg,int len,unsigned int flags,struct sockaddr *to int tolen)
sockfd,buf,len的意义和read,write一样,分别表示套接字描述符,发送或接收的缓冲区及大小.recvfrom负责从sockfd接收数据,如果from不是NULL,那么在from里面存储了信息来源的情况,如果对信息的来源不感兴趣,可以将from和fromlen设置为NULL.sendto负责向to发送信息.此时在to里面存储了收信息方的详细资料.
5.2 一个实例
/* 服务端程序 server.c */
#include& #include& #include& #include& #include& #define SERVER_PORT 8888& #define MAX_MSG_SIZE 1024
void udps_respon(int sockfd)& {& struct sockaddr_& int addrlen,n;& char msg[MAX_MSG_SIZE];
while(1)& { /* 从网络上度,写到网络上面去 */& n=recvfrom(sockfd,msg,MAX_MSG_SIZE,0,& (struct sockaddr*)&addr,&addrlen);& msg[n]=0;& /* 显示服务端已经收到了信息 */& fprintf(stdout,”I have received %s”,msg);& sendto(sockfd,msg,n,0,(struct sockaddr*)&addr,addrlen);& }& }
int main(void)& {&& struct sockaddr_
sockfd=socket(AF_INET,SOCK_DGRAM,0);& if(sockfd&0)& {& fprintf(stderr,”Socket Error:%s\n”,strerror(errno));& exit(1);& }& bzero(&addr,sizeof(struct sockaddr_in));& addr.sin_family=AF_INET;& addr.sin_addr.s_addr=htonl(INADDR_ANY);& addr.sin_port=htons(SERVER_PORT);& if(bind(sockfd,(struct sockaddr *)&ddr,sizeof(struct sockaddr_in))&0)& {& fprintf(stderr,”Bind Error:%s\n”,strerror(errno));& exit(1);& }& udps_respon(sockfd);& close(sockfd);& }
/* 客户端程序 */& #include& #include& #include& #include& #include& #include& #define MAX_BUF_SIZE 1024
void udpc_requ(int sockfd,const struct sockaddr_in *addr,int len)& {& char buffer[MAX_BUF_SIZE];&& while(1)& { /* 从键盘读入,写到服务端 */& fgets(buffer,MAX_BUF_SIZE,stdin);& sendto(sockfd,buffer,strlen(buffer),0,addr,len);& bzero(buffer,MAX_BUF_SIZE);& /* 从网络上读,写到屏幕上 */& n=recvfrom(sockfd,buffer,MAX_BUF_SIZE,0,NULL,NULL);& buffer[n]=0;& fputs(buffer,stdout);& }& }
int main(int argc,char **argv)& {& int sockfd,& struct sockaddr_
if(argc!=3)& {& fprintf(stderr,”Usage:%s server_ip server_port\n”,argv[0]);& exit(1);& }
if((port=atoi(argv[2]))&0)& {& fprintf(stderr,”Usage:%s server_ip server_port\n”,argv[0]);& exit(1);& }
sockfd=socket(AF_INET,SOCK_DGRAM,0);& if(sockfd&0)& {& fprintf(stderr,”Socket Error:%s\n”,strerror(errno));& exit(1);& }& /* 填充服务端的资料 */& bzero(&addr,sizeof(struct sockaddr_in));& addr.sin_family=AF_INET;& addr.sin_port=htons(port);& if(inet_aton(argv[1],&addr.sin_addr)&0)& {& fprintf(stderr,”Ip error:%s\n”,strerror(errno));& exit(1);& }& udpc_requ(sockfd,&addr

我要回帖

更多关于 不小心装了双系统 的文章

 

随机推荐