QQ登录

只需一步,快速开始

 找回密码
 注册

QQ登录

只需一步,快速开始

查看: 1100|回复: 12

网络编程的函数

[复制链接]
发表于 2003-9-23 17:16:06 | 显示全部楼层 |阅读模式
那个有关于LINUX网络编程的资料,
我最希望是有关网络编程函数的介绍.全面一些的
发表于 2003-9-24 10:53:06 | 显示全部楼层

Re: 网络编程的函数

以下是我的学习笔记,拿来跟大家分享,
参考文献《Proxy源代码分析》

首先要弄清端式问题,它是计算机结构查看存储器方式中的一个基本问题。简单的说就是以高字节顺序存储或以低字节顺序存储。由这引出网络字节次序(NBO)和主机字节次序(HBO),网络字节次序规定为以高字节顺序存储,而主机字节次序可高可低,所以当计算机与网络进行通讯时,就涉及到一个字节次序转换的问题。这有四个函数:ntohs(),htons(),ntohl(),htonl(),其中,n代表net,h代表host,s代表short,l代表long。

    下面先总结一下常用的socket函数:
    socket                               创建一个套接字  
    bind                                   绑定端口到一个套接字
    listen                                 允许一个服务器套接字接受连接
    accept                               通过一个服务器套接字接受一个客户
    connect                             连接一个客户套接字到服务器
    select                                在许多套接字上识别套接字读/写/异常
    send/sendto                     通过一个套接字发送数据
    recv/recvfrom                    从一个套接字接收数据
    getsockopt/setsockout      获得或设置套接字/协议选项
    getpeername/getsockname  在套接字的本地和远程端上获得地址/端口信息
    close                                      关闭套接字
    shutdown                               关闭一个带控制的套接字
    gethostbyname/gethostbyaddr    使用域名服务器解析名字或地址
    inet_addr/inet_aton                转换字符串IP地址到二进制网络字节次序的地址
    inet_ntoa                              转换二进制网络字节次序的地址到一个字符串
    [/list:u]
    下面将说明一些常用的函数。
    在此之前先说一下套接字地址结构设置的问题。
    在通常情况下,首先要将描述服务器信息的套接字地址结构清零,然后在地址结构中填入相应的内容,准备接受客户机送来的连接建立请求。这个清零操作可以用多种字节处理函数来实现,例如bzero()、bcopy()、memset()、memcpy()等,以字母"b"开始的两个函数是和BSD系统兼容的,而后面两个是ANSI C提供的函数。这段代码中使用的bzero()其描述为:
       void bzero(void *s, int n);
      函数的具体操作是将参数s指定的内存的前n个字节清零。memset()同样也很常用,其描述为:
       void *memset(void *s, int c, size_t n);
      具体操作是将参数s指定的内存区域的前n个字节设置为参数c的内容。(以_t结尾的数据类型被称为原始系统数据类型,通常在头文件sys/types.h中定义)
      下一步就是在已经清零的服务器套接字地址结构中填入相应的内容。Linux系统的套接字是一个通用的网络编程接口,它应该支持多种网络通信协议,每一种协议都使用专门为自己定义的套接字地址结构(例如TCP/IP网络的套接字地址结构就是struct sockaddr_in)。不过为了保持套接字函数调用参数的一致性,Linux系统还定义了一种通用的套接字地址结构:
    -----------------------------------------------------------------
    <linux/socket.h>
    struct sockaddr
    {
      unsigned short sa_family; /* address type */
      char sa_data[14]; /* protocol address */
    }
    -----------------------------------------------------------------
      其中sa_family意指套接字使用的协议族地址类型,对于我们的TCP/IP网络,其值应该是AF_INET,sa_data中存储具体的协议地址,不同的协议族有不同的地址格式。这个通用的套接字地址结构一般不用做定义具体的实例,但是常用做套接字地址结构的强制类型转换,如我们经常可以看到这样的用法:
       bind(sockfd,(struct sockaddr *) &servaddr,sizeof(servaddr))
      用于TCP/IP协议族的套接字地址结构是sockaddr_in,其定义为:
    -----------------------------------------------------------------
    <linux/in.h>
    struct in_addr
    {
      __u32 s_addr;
    };
      struct sochaddr_in
    {
      short int sin_family;
      unsigned short int sin_port;
      struct in_addr sin_addr;
    };
    -----------------------------------------------------------------
     其成员的设置一般是这样的:
       servaddr.sin_family = AF_INET;
      表示套接字使用TCP/IP协议族。
       servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
      设置服务器套接字的IP地址为特殊值INADDR_ANY,这表示服务器愿意接收来自任何网络设备接口的客户机连接。
       servaddr.sin_port = htons(PORT);
    设置通信端口号,PORT应该是我们已经定义好的。

    创建通信套接字
    #include <sys/types.h>
    #include <sys/socket.h>
    int socket(int domain, int type, int protocol)
    domain:指定套接字使用的协议族。选项包括TCP/IP协议族的AF_INET,UNIX协议族的AF_UNIX,ISO协议族的AF_ISO,路由协议的AF_ROUTE.
    type:指定协议类型。选项有SOCK_RAW(对原始的或IPv4),SOCK_STREAM(对TCP),SOCK_DGRAM(对UDP)这些类型只对AF_INET协议族有效。
    protocol:总是0(数字0),除非类型字段被定义为SOCK_RAW,不常用。
    当socket()函数成功执行时,返回一个标志这个套接字的描述符,如果出错则返回"-1",并设置errno为相应的错误类型。(errno在errno.h中定义,用来标志错误类型,不用定义可直接使用)
    例子:
    TCP创建套接字:
    int sockfd;
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    UDP创建套接字:
    int sockfd;
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);

    服务器公开地址

      如果服务器要接受客户机的连接请求,那么它必须先要在整个网络上公开自己的地址。在设置了服务器的套接字地址结构之后,可以通过调用函数bind()绑定服务器的地址和套接字来完成公开地址的操作。函数bind()的详细描述为:
    -----------------------------------------------------------------
    #include <sys/types.h>
    #include <sys/socket.h>
      int bind(int sockfd, struct sockaddr *addr, int addrlen);
    -----------------------------------------------------------------
      参数sockfd是我们通过调用socket()创建的套接字描述符。参数addr是本机地址,参数addrlen是套接字地址结构的长度。函数执行成功时返回"0",否则返回"-1",并设置errno变量为EADDRINUAER。
      如果是服务器调用bind()函数,如果设置了套接字的IP地址为某个本地IP地址,那么这表示服务器只接受来自于这个IP地址的特定主机发出的连接请求。不过一般情况下都是将IP地址设置为INADDR_ANY,以便接受所有网络设备接口送来的连接请求。
      客户机一般是不会调用bind()函数的,因为客户机在连接时不用指定自己的套接字地址端口号,系统会自动为客户机选择一个未用端口号,并且用本地IP地址自动填充客户机套接字地址结构中的相应项。但是在某些特定的情况下客户机需要使用特定的端口号,例如Linux中的rlogin命令就要求使用保留端口号,而系统是不能为客户机自动分配保留端口号的,这就需要调用bind()来绑定一个保留端口号了。不过在一些特殊的环境下,这样绑定特定端口号也会带来一些负面影响,如在HTTP服务器进入TIME_WAIT状态后,客户机如果要求再次与服务器建立连接,则服务器会拒绝这一连接请求。如果客户机最后进入TIME_WAIT状态,则马上再次执行bind()函数时会返回出错信息"-1",原因是系统会认为同时有两次连接绑定同一个端口。

    转换Listening套接字

      接下来,服务器需要将我们刚才与IP地址和端口号完成绑定的套接字转换成倾听listening套接字。只有服务器程序才需要执行这一步操作。我们通过调用函数listen()实现这一操作。listen()的详细描述为:
    -----------------------------------------------------------------
    #include <sys/socket.h>
    int listen(int sockfd, int backlog);
    -----------------------------------------------------------------
      参数sockfd指定我们要求转换的套接字描述符,参数backlog设置请求队列的最大长度。函数listen()主要完成以下操作。
      首先是将套接字转换成倾听套接字。因为函数socket()创建的套接字都是主动套接字,所以客户机可以通过调用函数connect()来使用这样的套接字主动和服务器建立连接。而服务器的情况恰恰相反,服务器需要通过套接字接收客户机的连接请求,这就需要一个"被动"套接字。listen()就可将一个尚未连接的主动套接字转换成为这样的"被动"套接字,也就是倾听套接字。在执行了listen()函数之后,服务器的TCP就由CLOSED变成LISTEN状态了。
    另外listen()可以设置连接请求队列的最大长度。虽然参数backlog的用法非常简单,只是一个简单的整数。但搞清楚请求队列的含义对理解TCP协议的通信过程建立非常重要。TCP协议为每个倾听套接字实际上维护两个队列,一个是未完成连接队列,这个队列中的成员都是未完成3次握手的连接;另一个是完成连接队列,这个队列中的成员都是虽然已经完成了3次握手,但是还未被服务器调用accept()接收的连接。参数backlog实际上指定的是这个倾听套接字完成连接队列的最大长度。

    接收连接

    accept()的详细描述为:
    -----------------------------------------------------------------
    #include <sys/socket.h>
      int accept(int sockfd, struct sockaddr *addr, int *addrlen);
    -----------------------------------------------------------------
      参数sockfd是我们转换成功的倾听套接字描述符;参数addr是一个指向套接字地址结构的指针,参数addrlen为一个整型指针。当函数成功执行时,返回3个结果,函数返回一个新的套接字描述符,服务器可以通过这个新的套接字描述符和客户机进行通信。参数addr所指向的套接字地址结构中将存放客户机的相关信息,addrlen指针将描述前述套接字地址结构的长度。在通常情况下服务器对这些信息不是很感兴趣,因此我们经常可以看到一些源代码中将accept()函数的后两个参数都设置为NULL。在一段服务器程序中,可以始终只用一个倾听套接字来接收多个客户机的连接请求;而如果我们要和客户机建立一个实际的连接的话,对每一个请求我们都需要调用accept()返回一个新的套接字。当服务器处理完毕客户机的请求后,一定要将相应的套接字关闭;如果整个服务器程序将要结束,那么一定要将倾听套接字关闭。如果accept()函数执行失败,则返回"-1",如果accept()函数阻塞等待客户机调用connect()建立连接,进程在此时恰好捕捉到信号,那么函数在返回"-1"的同时将变量errno的值设置为EINTR。这和accept()函数执行失败是有区别的。因此我们可以看到这样的语句:
    -----------------------------------------------------------------
    if (newsockfd < 0 && errno == EINTR)
    continue;
    /* a signal might interrupt our accept() call */
    else if (newsockfd < 0)
    /* something quite amiss -- kill the server */
    errorout("failed to accept connection");
    -----------------------------------------------------------------
    其中newsockfd为accept返回值。
      可以看出程序在处理这两种情况时操作是完全不同的,同样是accept()返回"-1",如果有errno == EINTR,那么系统将再次调用accept()接受连接请求,否则服务器进程将直接结束。

    处理客户机请求

      当服务器与客户机建立连接以后,就可以处理客户机的请求了。一般情况下服务器程序都要创建一个子进程用于处理客户机请求;而父进程则继续监听,时刻准备接受其它客户机的连接请求。在大型的服务器程序中,一般都要在子进程里,根据客户机请求的不同而通过exec()系列函数调用不同的处理程序,这也是在学习linux/Unix编程中一个非常重要的地方。

    连接服务器

    函数connect()的定义为:
    -----------------------------------------------------------------
    #include <sys/types.h>
    #include <sys/socket.h>
    int connect(int sockfd, struct sockaddr *servaddr, int addrlen);
    -----------------------------------------------------------------
      参数sockfd是调用函数socket()返回的套接字描述符,参数servaddr指向远程服务器的套接字地址结构,参数addrlen指定这个套接字地址结构的长度。函数connect()执行成功时返回"0",如果执行失败则返回"-1",并将全局变量errno设置为相应的错误类型。一般常用switch()函数对以下三种出错类型进行处理: ETIMEDOUT、ECONNREFUSED和ENETUNREACH。这三个出错类型的意思分别为:ETIMEDOUT代表超时,产生这种情况的原因有很多,最常见的是服务器忙,无法应答客户机的连接请求;ECONNREFUSED代表连接拒绝,即服务器端没有准备好的倾听套接字,或是没有对倾听套接字的状态进行监听;ENETUNREACH表示网络不可达。

    I/O多路复用

    select()函数可以构造一个表,在这个表中包含了我们所有要用到的文件描述符。然后我们可以调用一个函数,这个函数可以检测这些文件描述符的状态,当某个(我们指定的)文件描述符准备好进行I/O操作时,此函数就返回,告知进程哪个文件描述符已经可以执行I/O操作了。这样就避免了长时间的阻塞。select()系列函数的详细描述为:
    -----------------------------------------------------------------
    #include <sys/time.h>
    #include <sys/types.h>
    #include <unistd.h>
    int select(int n, fd_set *readfds, fd_set *writefds, fd_est *exceptfds, struct timeval *timeout);
    FD_CLR(int fd, fd_set *set);
    FD_ISSET(int fd, fd_set *set);
    FD_SET(int fd, fd_set *set);
    FD_ZERO(fd_set *set);
    -----------------------------------------------------------------
      select()函数将创建一个我们所关心的文件描述符表,它的参数将在内核中为这些文件描述符设置我们所关心的条件,例如是否是可读、是否可写以及是否异常,而且在参数中还可以设置我们希望等待的最大时间。在select()成功执行时,它将返回目前已经准备好的描述符数量,同时内核可以告诉我们各个描述符的状态信息。如果超时,则返回"0",如果出错,则函数返回"-1",并同时设置errno为相应的值。
      select()的最后一个参数timeout将设置等待时间。其中结构timeval是在文件<bits/time.h>中定义的。
    -----------------------------------------------------------------
    struct timeval
    {
      __time_t tv_sec; /* Seconds */
      __time_t tv_usec; /* Microseconds */
    };
    -----------------------------------------------------------------
      参数timeout的设置有三种情况。timeout==NULL时,这表示用户希望永远等待,直到我们指定的文件描述符中的一个已准备好,或者是捕捉到一个信号。如果是由于捕捉到信号而中断了这个无限期的等待过程的话,select()将返回"-1",同时设置errno的值为EINTR。
      如果timeout-&gt;tv_sec==0&amp;&amp;timeout-&gt;tv_usec==0,那么这表示完全不等待。Select()测试了所有指定文件描述符后立即返回。这是得到多个描述符状态而不阻塞select()函数的轮询方法。
      如果timeout-&gt;tv_sec!=0||timeout-&gt;tv_usec!=0,那么这两个参数的值即为我们希望函数等待的时间。其中tv_sec设置时间单位为秒,tv_usec设置时间单位为微秒。如果在超时的时候,在我们指定的所有文件描述符里面仍然没有任何一个准备好的话,则select()将返回"0"。
      中间三个参数的数据类型是fd_set,它的意思是文件描述符集,而readfds, writefds和exceptfds则分别是指向文件描述符集的指针,他们分别描述了我们所关心的可读、可写以及状态异常的各个文件描述符。之所以我们称select()可以创建一个文件描述符"表",那个所谓的表就是由这三个参数指向的数据结构组成的。其中在每个set_fd数据类型中都为我们关心的所有文件描述符保留了一位。所以在监测文件描述符状态的时候,就在这些set_fd数据结构中查询相关的位。
      第一个参数n用来说明到底需要遍历多少个描述符位。n的值一般是这样设置的,从我们关心的所有文件描述符中选出最大值再加1。例如我们设置的所有文件描述符中最大的为6,那么将n设置为7,则系统在检测描述符状态的时候,就只用遍历前7位(fd0~fd6)的状态。不过如果不想这样麻烦的话,我们可以将n的值直接设置为FD_SETSIZE。这是系统中设定的最大文件描述符个数,不同的系统这个值也不相同,一般是256或是1024。这样在检测描述符状态的时候,函数将遍历所有的描述符位。
    在调用select()函数实现多路I/O转接时,首先我们要声明一个新的文件描述符集:
          fd_set rdfdset;
      然后调用FD_ZERO()清空此文件描述符集的所有位,以免下面检测描述符位的时候返回错误结果:
       FD_ZERO(&amp;rdfdset);
      然后调用FD_SET()在文件描述符集中设置我们关心的位。假设有读写两种描述符readsockfd和writesockfd,则:
    FD_SET(readsockfd,&amp;rdfdset);
    FD_SET(writesockfd,&amp;rdfdset);
    执行这样的select()函数:
       select(FD_SETSIZE,&amp;rdfdset,NULL,NULL,NULL)
      那么在select()返回后怎样检测set_fd数据结构中描述符位的状态呢?这就要调用函数FD_ISSET(),如果对应文件描述符的状态为"已准备好"(即描述符位为"1"),则FD_ISSET()返回"1",否则返回"0"。
    -----------------------------------------------------------------
    if (FD_ISSET(readsockfd,&amp;rdfdset)) {
      if ((iolen = read(readsockfd,buf,sizeof(buf))) &lt;= 0)
      break; /* zero length means the host disconnected */
      write(writesockfd,buf,len);
    -----------------------------------------------------------------
      这一段代码就实现从套接字readsockfd到套接字writesockfd的无阻塞传输。而下一段代码实现反方向的无阻塞传输:
    -----------------------------------------------------------------
    if (FD_ISSET(writesockfd,&amp;rdfdset)) {
      if ((iolen = read(writesockfd,buf,sizeof(buf))) &lt;= 0)
       break; /* zero length means the host disconnected */
       write(readsockfd,buf,len);

    最后说一下域名转换

    gethostbyname()函数
    #include &lt;netdb.h&gt;
    struct hostent *gethostbyname(const char *hostname);
    -----------------------------------------------------------------
      参数hostname指向我们需要转换的域名地址,函数直接返回转换结果,如果函数执行成功,则结果直接返回到一个指向hostent结构的指针中,否则返回空指针NULL。
    struct hostent的详细描述为:
    -----------------------------------------------------------------
    struct hostent {
       char *h_name;
       char **h_aliases;
       int h_addrtype;
       int h_length;
       char **h_addr_list;
    };
    #define h_addr h_addrlist[0]
    -----------------------------------------------------------------
      hostent成员的含义是h_name代表主机在网络上的的正式名称,h_aliases是所有主机别名的列表,h_addrtype是指主机的地址类型,一般设置为TCP/IP协议族AF_INET,h_length是主机的地址长度,一般设置为4个字节。h_addr_list是主机的IP地址列表。
    inet_addr()函数的描述为:
    -----------------------------------------------------------------
    #include &lt;sys/socket.h&gt;
    #include &lt;netinet/in.h&gt;
    #include &lt;arpa/inet.h&gt;
    unsigned long int inet_addr(const char *cp)
    -----------------------------------------------------------------
      inet_addr()的作用就是将参数cp指向的Internet主机地址从数字/点的形式转换成二进制形式并同时转换为网络字节顺序,并将转换结果直接返回。如果cp指向的IP地址不可用,则函数返回INADDR_NONE或"-1"。
    但有个问题:IP地址255.255.255.255绝对是一个有效地址,那么其二进制返回值也将是"-1",因此inet_addr()无法对这个IP地址进行处理。而函数inet_aton()则采用了一种更好的方法来返回出错信息,它的具体描述为:
    -----------------------------------------------------------------
    #include &lt;sys/socket.h&gt;
    #include &lt;netinet/in.h&gt;
    #include &lt;arpa/inet.h&gt;
    int inet_aton(const char *cp, struct in_addr *inp)
    -----------------------------------------------------------------
      函数执行成功时返回非零,转换结果存入指针inp指向的in_addr结构(前面介绍过)。如果参数cp指向的IP地址不可用,则返回"0"。这就避免发生inet_addr()那样的问题。


    就到这里吧,休息休息
回复

使用道具 举报

发表于 2003-9-24 10:56:00 | 显示全部楼层
第一次写这么长的帖子真累啊,可能有很多错误,大家别客气!
由于网上很多源码,这里就不帖例程了,大家去找个看看,这样学的才透彻!
回复

使用道具 举报

发表于 2003-9-24 11:13:32 | 显示全部楼层
我晕看了一遍之后发现落了两个最重要的函数send()和recv()   
#include &lt;sys/types.h&gt;
#include &lt;sys/socket.h&gt;
int send(int sockfd, const void *msg, int msglen, unsigned int flags)

int recv(int sockfd, const void *msg, int msglen, unsigned int flags)

flag通常为0    其他的很容易明白吧。
回复

使用道具 举报

发表于 2003-9-25 09:13:59 | 显示全部楼层
没人评价一下吗? :-(
回复

使用道具 举报

 楼主| 发表于 2003-9-26 08:08:03 | 显示全部楼层
好,谢谢rocklqk
回复

使用道具 举报

发表于 2003-9-26 14:14:51 | 显示全部楼层
希望对你有用!
回复

使用道具 举报

发表于 2003-9-26 18:49:57 | 显示全部楼层
很好啊,最好有电子版的全一点的
unp英文版的能找到吗?
回复

使用道具 举报

发表于 2003-9-27 08:55:38 | 显示全部楼层
unp是什么?这是我自己的学习笔记,呵呵,俺现在还不到出书的水平吧!
回复

使用道具 举报

发表于 2003-9-27 10:01:35 | 显示全部楼层
我指的是stevens的unix network programming 的卷1,卷2。
我中文的倒是有,但是还是英文的精准。
回复

使用道具 举报

发表于 2003-9-27 14:45:22 | 显示全部楼层
这个我没有。
回复

使用道具 举报

发表于 2003-9-27 18:37:25 | 显示全部楼层
没关系啦,最重要的时候问题的时候能找到兄弟解决。
回复

使用道具 举报

发表于 2003-9-28 09:16:07 | 显示全部楼层
:-)
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

GMT+8, 2024-11-14 11:13 , Processed in 0.075175 second(s), 15 queries .

© 2021 Powered by Discuz! X3.5.

快速回复 返回顶部 返回列表