Socket编程基础知识

端口号常识: 

端口号被从1 开始分配。
    通常端口号超出255 的部分被本地主机保留为私有用途。
    1到255 之间的号码被用于远程应用程序所请求的进程和网络服务。
    每个网络通信循环地进出主计算机的TCP 应用层。它被两个所连接的号码唯一地识别。这两个号码合起来叫做套接字.
        组成套接字的这两个号码就是机器的IP 地址和TCP 软件所使用的端口号。

套接字的三种类型 
    流式套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM)及原始套接字(RAW)。

  流式套接字(SOCK_STREAM) 
    流式的套接字可以提供可靠的、面向连接的通讯流。
    如果你通过流式套接字发送了顺序的数据:“1”、“2”。那么数据到达远程时候的顺序也是“1”、“2”。
    Telnet 应用程序、BBS服务、以及系统的远程登陆都是通过Telnet 协议连接的。Telnet 就是一个流式连接。你是否希望你在Telnet 应用程序上输入的字符(或汉字)在到达远程应用程序的时候是以你输入的顺序到达的?答案应该是肯定的吧。
    还有WWW 浏览器,它使用的HTTP 协议也是通过流式套接字来获取网页的。事实上,如果你Telnet 到一个Web Site 的80 端口上,然后输入 “GET 网页路径名”然后按两下回车(或者是两下Ctrl+回车)然后你就得到了“网页路径名”所代表的网页!

  数据报套接字(SOCK_DGRAM) 
    数据报套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证可靠,无差错。
       如果你发送了一个数据报,它可能不会到达。
       它可能会以不同的顺序到达。
       如果它到达了,它包含的数据中可能存在错误。
       数据报套接字也使用IP,但是它不使用TCP,它使用使用者数据报协议UDP(User Datagram Protocol 可以参考RFC 768)

为什么说它们是“无连接”的呢?因为它(UDP)不像流式套接字那样维护一个打开的连接,你只需要把数据打成一个包,把远程的IP 贴上去,然后把这个包发送出去。这个过程是不需要建立连接的。
       UDP 的应用例子有: tftp, bootp 等。

那么,数据包既然会丢失,怎样能保证程序能够正常工作呢?
       事实上,每个使用UDP的程序都要有自己的对数据进行确认的协议。
           比如, TFTP 协议定义了对于每一个发送出去的数据包,远程在接受到之后都要回送一个数据包告诉本地程序:“我已经拿到了!”(一个 “ACK” 包)。如果数据包发的送者在5 秒内没有的得到回应,它就会重新发送这个数据包直到数据包接受者回送了 “ACK” 信号。这些知识对编写一个使用UDP 协议的程序员来说是非常必要的。

无连接服务器一般都是面向事务处理的,一个请求一个应答就完成了客户程序与服务程序之间的相互作用。

面向连接服务器处理的请求往往比较复杂,不是一来一去的请求应答所能解决的,而且往往是并发服务器

套接字工作过程如下:
         服务器首先启动
         通过调用socket()建立一个套接字,
         然后调用bind()将该套接字和本地网络地址联系在一起,
         再调用listen()使套接字做好侦听的准备,并规定它的请求队列的长度,
         之后就调用accept()来接收连接。

客户在建立套接字
         然后就可调用connect()和服务器建立连接。
 
         客户机和服务器之间就可以通过调用read()和write()来发送和接收数据。

最后,待数据传送结束后,双方调用close()关闭套接字。

对流式套接字你所需要做的只是调用send() 函数来发送数据。而对于数据报套接字,你需要自己加个信息头,然后调用sendto() 函数把数据发送出去

  原始套接字 
     原始套接字主要用于一些协议的开发,可以进行比较底层的操作。它功能强大,但是没有上面介绍的两种套接字使用方便,一般的程序也涉及不到原始套接字

套接字结构

struct sockaddr
这个结构用来存储套接字地址。
数据定义:
struct sockaddr 
{
    unsigned short sa_family;  /* address族, AF_xxx */
    char sa_data[14];              /* 14 bytes的协议地址 */
};

sa_family  一般来说,都是 “AF_INET”。
sa_data    包含了一些远程电脑的地址、端口和套接字的数目,它里面的数据是杂溶在一切的。

为了处理struct sockaddr, 程序员建立了另外一个相似的结构struct sockaddr_in (“in” 代表 “Internet”):

struct sockaddr_in 
{
   short     int       sin_family;           /* Internet地址族 */
   unsigned  short int sin_port;       /* 端口号 */
   struct    in_addr   sin_addr;         /* Internet地址 */
   unsigned  char      sin_zero[8];   /* 添0(和struct sockaddr一样大小)*/
};

注意:
    1)这个结构提供了方便的手段来访问socket address(struct sockaddr)结构中的每一个元素。
    2)sin_zero[8] 是为了是两个结构在内存中具有相同的尺寸
                  要把sin_zero 全部设成零值(使用bzero()或memset()函数)。
    3)一个指向struct sockaddr_in 的指针可以声明指向一个sturct sockaddr 的结构。所以虽然socket() 函数需要一个structaddr * ,你也可以给他一个sockaddr_in * 。
    4)在struct  sockaddr_in 中,sin_family 相当于 在struct sockaddr 中的sa_family,需要设成 “AF_INET”。
    5)一定要保证sin_port 和sin_addr 必须是网络字节顺序(见下节)!

2.struct in_addr  ( 因特网地址 (a structure for historical reasons) )
   struct in_addr 
   {
       unsigned long s_addr;
   };

如果你声明了一个 "ina" 作为一个struct sockaddr_in 的结构, 那么“ina.sin_addr.s_addr”就是4 个字节的IP 地址(按网络字节顺序排放)。
   需要注意的是,即使你的系统仍然使用联合而不是结构来表示struct in_addr,你仍然可以用上面的方法得到4 个字节的IP 地址(一些 #defines 帮了你的忙)

网络字节顺序 
   因为每一个机器内部对变量的字节存储顺序不同(有的系统是高位在前,底位在后,而有的系统是底位在前,高位在后),而网络传输的数据大家是一定要统一顺序 的。
   所以对与内部字节表示顺序和网络字节顺序不同的机器,就一定要对数据进行转换(比如IP 地址的表示,端口号的表示)。
   但是内部字节顺序和网络字节顺序相同的机器该怎么办呢?是这样的:它们也要调用转换函数,但是真正转换还是不转换是由系统函数自己来决定的。

有关的转化函数
      我们通常使用的有两种数据类型:短型(两个字节)和长型(四个字节)。
      下面介绍的这些转换函数对于这两类的无符号整型变量都可以进行正确的转换。
      如果你想将一个短型数据从主机字节顺序转换到网络字节顺序的话,有这样一个函数htons:
         它是以“h”开头的(代表“主机”);
         紧跟着它的是“to”,代表“转换到”;
         然后是“n”代表“网络”;
         最后是“s”,代表“短型数据”。
         H-to-n-s,就是htons() 函数(可以使用Hostto Network Short 来助记)

你可以使用 “n”,“h”,“to”,“s”,“l”的任意组合.当然,你要在可能的情况下进行组合。比如,系统是没有stolh() 函数的(Short to Long Host?)。
        下面给出套接字字节转换程序的列表:
          htons()——“Host to Network Short” 主机字节顺序转换为网络字节顺序(对无符号短型进行操作4 bytes)
          htonl()——“Host to Network Long” 主机字节顺序转换为网络字节顺序(对无符号长型进行操作8 bytes)
          ntohs()——“Network to Host Short “ 网络字节顺序转换为主机字节顺序(对无符号短型进行操作4 bytes)
          ntohl()——“Network to Host Long “ 网络字节顺序转换为主机字节顺序(对无符号长型进行操作8 bytes)

在struct sockaddr_in 中的sin_addr 和sin_port 他们的字节顺序都是网络字节顺序,而sin_family 却不是网络字节顺序的。为什么呢?
        这个是因为sin_addr 和sin_port 是从IP 和UDP 协议层取出来的数据,而在IP 和UDP协议层,是直接和网络相关的,所以,它们必须使用网络字节顺序。
        然而, sin_family 域只是内核用来判断struct sockaddr_in 是存储的什么类型的数据,并且, sin_family 永远也不会被发送到网络上,所以可以使用主机字节顺序来存储

socket() 函数 
   
   /**
   * 
   * 取得套接字描述符!(记得我们以前说过的吗?它其实就是一个文件描述符)
   *
   * domain   需要被设置为 “AF_INET”,就像上面的struct sockaddr_in。
   * type     参数告诉内核这个socket 是什么类型,“SOCK_STREAM”或是“SOCK_DGRAM”。
   * protocol 通常为0
   *
   * return   如果发生错误,socket()函数返回 –1 。全局变量errno 将被设置为错误代码。
   *
   */
   #include <sys/types.h>
   #include <sys/socket.h>
   int socket(int domain , int type , int protocol)

示例:
   if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
   {
        perror("create sock");
        return -1;
   }
   else
   {
      // printf("socket created.../n");
   }

bind() 函数

/**
*
* 为套接字绑定一个端口号
*
* 当你需要进行端口监听listen()操作,等待接受一个连入请求的时候,
* 一般都需要经过这一步。比如网络泥巴(MUD),Telnet a.b.c.d 4000

* 如果你只是想进行连接一台服务器,也就是进行connect() 操作的时候,这一步并不是必须的。 
*
*
* sockfd    是由socket()函数返回的套接字描述符
* my_addr   是一个指向struct sockaddr 的指针,包含有关你的地址的信息:名称、端口和IP 地址。
* addrlen   可以设置为sizeof(struct sockaddr)

* return    调用错误的时候,返回 -1 作为错误发生的标志。errno 的值为错误代码。
*
*/
#include <sys/types.h>
#include <sys/socket.h>
int bind (int sockfd , struct sockaddr *my_addr , int addrlen) ;

示例:
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#define  MYPORT 4000
main()
{
  int sockfd ;
  struct sockaddr_in my_addr ;

sockfd                  = socket(AF_INET, SOCK_STREAM, 0); /* 在你自己的程序中要进行错误检查!! */
  my_addr.sin_family      = AF_INET ;        /* 主机字节顺序 */
  my_addr.sin_port        = htons(MYPORT);   /* 网络字节顺序,短整型 */
  my_addr.sin_addr.s_addr = inet_addr("166.111.69.52") ;
  bzero(&(my_addr.sin_zero), 8);             /* 将整个结构剩余部分数据设为0 */

bind (sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));  /* 不要忘记在你自己的程序中加入判断bind 错误的代码!! */

注意:
    my_addr.sin_port         是网络字节顺序, 短整型
    my_addr.sin_addr.s_addr  也是网络字节顺序。

最后,bind()可以在程序中自动获取你自己的IP 地址和端口。
    my_addr.sin_port = 0 ;                 /* 随机选择一个端口 */
    my_addr.sin_addr.s_addr = INADDR_ANY ; /* 使用自己的地址 */
    如上,通过设置my_addr.sin_port 为0,bind()可以知道你要它帮你选择合适的端口;
    通过设置my_addr.sin_addr.s_addr 为INADDR_ANY,bind()知道你要它将s_addr 填充为运行这个进程的机器的IP。
    这一切都可以要求bind()来自动的帮助你完成。

如果你注意到了一些细节的话,你可能会发现我并没有将INADDR_ANY 转换为网络字节顺序!是这样的,INADDR_ANY的值为0,0 就是0,无论用什么顺序排列位的顺序,它都是不变的。

有读者会想了,因为我用的INADDR_ANY 是一个#define,那么如果将我的程序移植到另外一个系统,假如那里的INADDR_ANY是这样定义的:
       #define INADDR_ANY 100,
       那么我的程序不是就会不运行了吗?那么下面这段代码就OK 了:
       my_addr.sin_port = htons(0);                   /* 随机选择一个未用的端口 */
       my_addr.sin_addr.s_addr = htonl(INADDR_ANY) ;  /* 使用自己的IP地址 */

现在我们已经是这么的严谨,对于任何数值的INADDR_ANY调用bind 的时候就都不会有麻烦了。

另外一件必须指出的事情是:
       当你调用bind()的时候,不要把端口数设置的过小!小于1024 的所有端口都是保留下来作为系统使用端口的,没有root 权利无法使用。你可以使用1024 以上的任何端口,一直到65535 :你所可能使用的最大的端口号(当然,你还要保证你所希望使用的端口没有被其他程序所使用)。

最后注意有关bind()的是:
       有时候你并不一定要调用bind()来建立网络连接。比如你只是想连接到一个远程主机上面进行通讯,你并不在乎你究竟是用的自己机器上的哪个端口进行通讯 (比如Telnet),那么你可以简单的直接调用connect()函数,connect()将自动寻找出本地机器上的一个未使用的端口,然后调用 bind()来将其socket 绑定到那个端口上。

connect() 函数

/*
*
* sockfd     套接字文件描述符,由socket()函数返回的
* serv_addr  是一个存储远程计算机的IP 地址和端口信息的结构
* addrlen    应该是sizeof(struct sockaddr)
*
* return     如果发生了错误(比如无法连接到远程主机,或是远程主机的指定端口无法进行连接等)它将会返回错误值 -1
*            全局变量errno将会存储错误代码
*
*
*/
#include <sys/types.h>
#include <sys/socket.h>

int connect (int sockfd, struct sockaddr *serv_addr, int addrlen);

示例:
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#define DEST_IP “166.111.69.52”
#define DEST_PORT 23
main()
{
   int sockfd ;

/* 将用来存储远程信息 */
   struct sockaddr_in dest_addr ;

/* 注意在你自己的程序中进行错误检查!! */
   sockfd = socket(AF_INET, SOCK_STREAM, 0);

/* 主机字节顺序 */
   dest_addr.sin_family = AF_INET ;

/* 网络字节顺序,短整型 */
   dest_addr.sin_port = htons(DEST_PORT(;

dest_addr.sin_addr.s_addr = inet_addr(DEST_IP);

/* 将剩下的结构中的空间置0 */
   bzero(&(dest_addr.sin_zero), 8);

/* 不要忘记在你的代码中对connect()进行错误检查!! */
   connect(sockfd, (struct sockaddr *)&dest_addr, sizeof(struct sockaddr));

注意我们没有调用bind()函数。基本上,我们并不在乎我们本地用什么端口来通讯,是不是?我们在乎的是我们连到哪台主机上的哪个端口上。Linux 内核自动为我们选择了一个没有被使用的本地端口。

listen() 函数

/*
*
* 等待别人连接,进行系统侦听请求
* 当有人连接你的时候,你有两步需要做:
*     通过listen()函数等待连接请求
*     然后使用accept()函数来处理
*
* 那么我们需要指定本地端口了,因为我们是等待别人的连接。所以,在listen()函数调用之前,我们需要使用bind() 函数来指定使用本地的哪一个端口数值
* 如果你想在一个端口上接受外来的连接请求的话,那么函数的调用顺序为:
*  socket();
*  bind();
*  listen();
*
* sockfd    是一个套接字描述符,由socket()系统调用获得
* backlog   是未经过处理的连接请求队列可以容纳的最大数目(每一个连入请求都要进入一个连入请求队列,等待listen 的程序调用accept()函数来接受这个连接。当系统还没有调用accept()函数的时候,如果有很多连接,那么本地能够等待的最大数目就是 backlog 的数值。你可以将其设成5 到10 之间的数值(推荐))
*
*
* return    错误返回-1, 并设置全局错误代码变量errno
*/

#include <sys/socket.h>
int listen(int sockfd, int backlog);

accept()函数 
/*
* 当调用它的时候,大致过程是下面这样的:
*     有人从很远很远的地方尝试调用connect()来连接你的机器上的某个端口(当然是你已经在listen()的)
*     他的连接将被listen 加入等待队列等待accept()函数的调用
*     你调用accept()函数,告诉他你准备连接
*
* sockfd   是正在listen() 的一个套接字描述符
* addr     一般是一个指向struct sockaddr_in 结构的指针;里面存储着远程连接过来的计算机的信息(比如远程计算机的IP 地址和端口)
* addrlen  是一个本地的整型数值,在它的地址传给accept() 前它的值应该是sizeof(struct sockaddr_in);accept()不会在addr 中存储多余addrlen bytes 大小的数据。如果accept()函数在addr 中存储的数据量不足addrlen,则accept()函数会改变addrlen 的值来反应这个情况。
*
*
* return   accept()函数将回返回一个新的套接字描述符,这个描述符就代表了这个连接 
*          这时候你有了两个套接字描述符:
*             返回给你的那个就是和远程计算机的连接, 这时候你所得到的那个新的套接字描述符就可以进行send()操作和recv()操作了。
*             而第一个套接字描述符仍然在你的机器上原来的那个端口上listen()
*
*          -1 来表明调用失败,同时全局变量errno 将会存储错误代码
*/

#include <sys/socket.h>
int accept(int sockfd, void *addr, int *addrlen);

示例:
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
/* 用户连接的端口号 */
#define MYPORT 4000

/* 等待队列中可以存储多少个未经过accept()处理的连接 */
#define BACKLOG 10

main()
{
   /* 用来监听网络连接的套接字sock_fd,用户连入的套接字使用new_fd */
   int sockfd, new_fd ;

/* 本地的地址信息 */
   struct sockaddr_in my_addr ;

/* 连接者的地址信息 */
   struct sockaddr_in their_addr ;

int sin_size;

/* 记得在自己的程序中这部分要进行错误检查! */
   sockfd = socket(AF_INET, SOCK_STREAM, 0) ;

/* 主机字节顺序 */
   my_addr.sin_family = AF_INET ;

/* 网络字节顺序,短整型 */
   my_addr.sin_port = htons(MYPORT) ;

/* 自动赋值为自己的IP */
   my_addr.sin_addr.s_addr = INADDR_ANY ;

/* 将结构中未使用部分全部清零 */
   bzero(&(my_addr.sin_zero), 8) ;

/* 不要忘记在你自己的程序中下面的程序调用需要进行错误检测!!*/
   bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));
 
   listen(sockfd, BACKLOG);
   sin_size = sizeof(struct sockaddr_in);
   new_fd = accept(sockfd, &their_addr, &sin_size);

send()、recv()函数
/*
*
*  这两个函数是最基本的,通过连接的套接字流进行通讯的函数
*  如果你想使用无连接的使用者数据报的话,请参sendto() 和recvfrom() 函数。

*  sockfd 是代表你与远程程序连接的套接字描述符。 
*  msg    是一个指针,指向你想发送的信息的地址。
*  len    是你想发送信息的长度
*  flags  发送标记。一般都设为0(你可以查看send 的man pages 来获得其他的参数值并且明白各个参数所代表的含义)
*  
*  return  函数在调用后会返回它真正发送数据的长度
*          -1  如果发生错误,错误代码存储在全局变量errno
*
*/

#include <sys/types.h>
#include <sys/socket.h>
int send(int sockfd, const void *msg, int len, int flags);

示例:
char *msg = "Hello! World! ";
int len, bytes_sent;

len = strlen(msg);
bytes_sent = send(sockfd, msg, len, 0);

注意:
   send() 所发送的数据可能少于你给它的参数所指定的长度!
   因为如果你给send()的参数中包含的数据的长度远远大于send()所能一次发送的数据,则send()函数只发送它所能发送的最大数据长度,然后它 相信你会把剩下的数据再次调用它来进行第二次发送。
   所以,记住如果send()函数的返回值小于len 的话,则你需要再次发送剩下的数据。
   幸运的是,如果包足够小(小于1K),那么send()一般都会一次发送光的。

recv()函数 
/*
*
*  sockfd  是你要读取数据的套接字描述符
*  buf     是一个指针,指向你能存储数据的内存缓存区域
*  len     是缓存区的最大尺寸
*  flags   是recv() 函数的一个标志,一般都为0 (具体的其他数值和含义请参考recv()的man pages)
*
*
*  返回    它所真正收到的数据的长度(也就是存到buf 中数据的长度)
*
*          -1    则代表发生了错误(比如网络以外中断、对方关闭了套接字连接等),全局变量errno 里面存储了错误代码
*/

#include <sys/types.h>
#include <sys/socket.h>
int recv(int sockfd, void *buf, int len, unsigned int flags);

sendto() 和recvfrom() 函数 
/*
* 这两个函数是进行无连接的UDP 通讯时使用的。
* 使用这两个函数,则数据会在没有建立过任何连接的网络上传输。因为数据报套接字无法对远程主机进行连接

* sockfd  是代表你与远程程序连接的套接字描述符
* msg     是一个指针,指向你想发送的信息的地址
* len     是你想发送信息的长度
* flags   发送标记。一般都设为0 (你可以查看send 的man pages 来获得其他的参数值并且明白各个参数所代表的含义)
* to      是一个指向struct sockaddr 结构的指针,里面包含了远程主机的IP 地址和端口数据
* tolen   只是指出了struct sockaddr 在内存中的大小sizeof(struct sockaddr)

* return  sendto()返回它所真正发送的字节数(当然也和send()一样,它所真正发送的字节数可能小于你所给它的数据的字节数)
*         -1 表示出错   同时全局变量errno 存储了错误代码
*/
#include <sys/types.h>
#include <sys/socket.h>
int sendto(int sockfd, const void *msg, int len, unsigned int flags, const struct sockaddr *to, int tolen);

recvfrom()函数
/*
*
* sockfd  是你要读取数据的套接字描述符
* buf     是一个指针,指向你能存储数据的内存缓存区域
* len     是缓存区的最大尺寸
* flags   是recv() 函数的一个标志,一般都为0 (具体的其他数值和含义请参考recv()的man pages)
* from    是一个本地指针,指向一个struct sockaddr 的结构(里面存有源IP 地址和端口数)
* fromlen 是一个指向一个int 型数据的指针,它的大小应该是sizeof (struct sockaddr).当函数返回的时候,formlen 指向的数据是form 指向的struct sockaddr 的实际大小.
*         如果一个信息大得缓冲区都放不下,那么附加信息将被砍掉。该调用可以立即返回,也可以永久的等待。这取决于你把flags 设置成什么类型。你甚至可以设置超时(timeout)值。
*
*
* return  返回它接收到的字节数,如果发生了错误,它就返回–1 ,全局变量errno存储了错误代码
*
*/

#include <sys/types.h>
#include <sys/socket.h>
int recvfrom(int sockfd, void *buf, int len, unsigned int flags, struct sockaddr *from, int *fromlen);

注意:
   如果你使用cnnect()连接到了一个数据报套接字的服务器程序上,那么你就可以使用send() 和recv() 函数来传输你的数据.
   不要以为你在使用一个流式的套接字,你所使用的仍然是一个使用者数据报的套接字,只不过套接字界面在send() 和recv()的时候自动帮助你加上了目标地址,目标端口的信息.

close()和shutdown()函数 
/*
* 程序进行网络传输完毕后,你需要关闭这个套接字描述符所表示的连接。实现这个非常简单,只需要使用标准的关闭文件的函数:close()。
* 执行close()之后,套接字将不会在允许进行读操作和写操作。任何有关对套接字描述符进行读和写的操作都会接收到一个错误。

*/
close(sockfd);

/*
* 如果你想对网络套接字的关闭进行进一步的操作的话,你可以使用函数shutdown()
* 它允许你进行单向的关闭操作,或是全部禁止掉。

* 如果你在一个未连接的数据报套接字上使用shutdown() 函数,它将什么也不做.
*
* sockfd 是一个你所想关闭的套接字描述符
* how 可以取下面的值。
*     0 表示不允许以后数据的接收操;
*     1 表示不允许以后数据的发送操作;
*     2 表示和close()一样,不允许以后的任何操作(包括接收,发送数据)
*
* return  0 执行成功
*        -1 执行失败, 全局变量errno 中存储了错误代码.
*   
*/
#include <sys/socket.h>
int shutdown(int sockfd, int how);

setsockopt() 和getsockopt() 函数
Linux 所提供的socket 库含有一个错误(bug)。
  此错误表现为你不能为一个套接字重新启用同一个端口号,即使在你正常关闭该套接字以后。
  例如,比方说,你编写一个服务器在一个套接字上等待的程序.服务器打开套接字并在其上侦听是没有问题的。无论如何,总有一些原因(不管是正常还是非正常的 结束程序)使你的程序需要重新启动。然而重启动后你就不能把它绑定在原来那个端口上了。从bind()系统调用返回的错误代码总是报告说你试图连接的端口 已经被别的进程所绑定。
  问题就是Linux 内核在一个绑定套接字的进程结束后从不把端口标记为未用。在大多数Linux/UNIX 系统中,端口可以被一个进程重复使用,甚至可以被其它进程使用。

在Linux 中绕开这个问题的办法是,当套接字已经打开但尚未有连接的时候用setsockopt()系统调用在其上设定选项(options)。
  setsockopt() 调用设置选项而getsockopt()从给定的套接字取得选项。

这里是这些调用的语法:
/*
*
*
* sockfd 必须是一个已打开的套接字
* level 是函数所使用的协议标准(protocol level)(TCP/IP 协议使用IPPROTO_TCP,套接字标准的选项实用SOL_SOCKET)
* name 选项在套接字说明书中(man page)有详细说明
* value 指向为getsockopt()函数所获取的值,setsockopt()函数所设置的值的地址
* optlen 指针指向一个整数,该整数包含参数以字节计算的长度
*
*
*
*
*
*/

#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);

当你打开一个套接字时必须同时用下面的代码段来调用setsockopt()函数:

/* 设定参数数值 */
opt = 1; 
len = sizeof(opt);

/* 设置套接字属性 */
setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&opt,&len);

getpeername()函数 
/*
* 这个函数可以取得一个已经连接上的套接字的远程信息(比如IP 地址和端口),告诉你在远程和你连接的究竟是谁
*
* 当你拥有了远程连接用户的IP 地址,你就可以使用inet_ntoa() 或gethostbyaddr()来输出信息或是做进一步的处理
*
* sockfd  是你想取得远程信息的那个套接字描述符
* addr    是一个指向struct sockaddr (或是struct sockaddr_in)的指针
* addrlen 是一个指向int 的指针,应该赋于sizeof(struct sockaddr)的大小
*
*
* return  错误,函数将返回 –1 ,并且错误代码储存在全局变量errno 中
*
*/
#include <sys/socket.h>
int getpeername(int sockfd, struct sockaddr *addr, int *addrlen);

gethostname()函数 
/*
* 可以取得本地主机的信息,它返回正在执行它的计算机的名字
* 返回的这个名字可以被gethostbyname()函数使用,由此可以得到本地主机的IP 地址
*
* hostname  是一个指向字符数组的指针,当函数返回的时候,它里面的数据就是本地的主机的名字
* size      是hostname 指向的数组的长度
*
*
* return    成功执行,它返回0
*           错误,则返回–1,全局变量errno 中存储着错误代码
*
*/
#include <unistd.h>
int gethostname(char *hostname, size_t size);

gethostbyname()函数 
/*
*
* 网络地址是以网络字节顺序存储的
*
*
* return    成功则返回指向结构struct hostent的指针
*
            #define h_addr h_addr_list[0]    //h_addr 是 h_addr_list 数组的第一个成员
            struct hostent 
            {
               char *h_name;       //是这个主机的正式名称
               char **h_aliases;   //是一个以NULL(空字符)结尾的数组,里面存储了主机的备用名称
               int  h_addrtype;    //是返回地址的类型,一般来说是“AF_INET”
               int  h_length;      //是地址的字节长度
               char **h_addr_list; //是一个以0 结尾的数组,存储了主机的网络地址
            };
*
*
*           如果发生错误,它将会返回NULL(但是errno 并不代表错误代码,h_errno 中存储的才识错误代码。参考下面的herror()函数
*/
struct hostent *gethostbyname(const char *name);

五种I/O 模式 
----------------------------------------
在Linux/UNIX 下,有下面这五种I/O 操作方式:
   阻塞I/O
   非阻塞I/O
   I/O 多路复用
   信号驱动I/O(SIGIO)
   异步I/O

程序进行输入操作有两步:
   等待有数据可以读
   将数据从系统内核中拷贝到程序的数据区。

对于一个对套接字的输入操作:
     第一步一般来说是,等待数据从网络上传到本地,当数据包到达的时候,数据将会从网络层拷贝到内核的缓存中;
     第二步是从内核中把数据拷贝到程序的数据区中

.阻塞I/O 模式
     简单的说,阻塞就是"睡眠"的同义词
         如你运行上面的listener 的时候,它只不过是简单的在那里等待接收数据。它调用recvfrom()函数,但是那个时候(listener 调用recvfrom()函数的时候),它并没有数据可以接收.所以recvfrom()函数阻塞在那里(也就是程序停在recvfrom()函数处睡大 觉)直到有数据传过来阻塞.你应该明白它的意思。

阻塞I/O 模式是最普遍使用的I/O 模式。大部分程序使用的都是阻塞模式的I/O 。
     缺省的,一个套接字建立后所处于的模式就是阻塞I/O 模式。

对于一个UDP 套接字来说,数据就绪的标志比较简单:
         已经收到了一整个数据报 
         没有收到。

而TCP 这个概念就比较复杂,需要附加一些其他的变量
         一个进程调用recvfrom ,然后系统调用并不返回知道有数据报到达本地系统,然后系统将数据拷贝到进程的缓存中。
        (如果系统调用收到一个中断信号,则它的调用会被中断)我们称这个进程在调用recvfrom 一直到从recvfrom 返回这段时间是阻塞的。
         当recvfrom正常返回时,我们的进程继续它的操作。

.非阻塞模式I/O 
    当我们将一个套接字设置为非阻塞模式,我们相当于告诉了系统内核:“当我请求的I/O 操作不能够马上完成,你想让我的进程进行休眠等待的时候,不要这么做,请马上返回一个错误给我。”
   
    如我们开始对recvfrom 的三次调用,因为系统还没有接收到网络数据,所以内核马上返回一个EWOULDBLOCK的错误。
    第四次我们调用recvfrom 函数,一个数据报已经到达了,内核将它拷贝到我们的应用程序的缓冲区中,然后recvfrom 正常返回,我们就可以对接收到的数据进行处理了。
  
    当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不听的测试是否一个文件描述符有数据可读(称做polling)。
    应用程序不停的polling 内核来检查是否I/O操作已经就绪。这将是一个极浪费CPU 资源的操作。这种模式使用中不是很普遍

.I/O 多路复用 select()
    在使用I/O 多路技术的时候,我们调用select()函数和poll()函数,在调用它们的时候阻塞,而不是我们来调用recvfrom(或recv)的时候阻塞。
    当我们调用select 函数阻塞的时候,select 函数等待数据报套接字进入读就绪状态。当select 函数返回的时候,也就是套接字可以读取数据的时候。这时候我们就可以调用recvfrom函数来将数据拷贝到我们的程序缓冲区中。
    和阻塞模式相比较,select()和poll()并没有什么高级的地方,而且,在阻塞模式下只需要调用一个函数:读取或发送,在使用了多路复用技术后, 我们需要调用两个函数了:先调用select()函数或poll()函数,然后才能进行真正的读写。
    
    多路复用的高级之处在于,它能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回

假设我们运行一个网络客户端程序,要同时处理套接字传来的网络数据又要处理本地的标准输入输出。在我们的程序处于阻塞状态等待标准输入的数据的时候,假如 服务器端的程序被kill(或是自己Down 掉了),那么服务器程端的TCP 协议会给客户端(我们这端)的TCP 协议发送一个FIN 数据代表终止连接。但是我们的程序阻塞在等待标准输入的数据上,在它读取套接字数据之前(也许是很长一段时间),它不会看见结束标志.我们就不能够使用阻 塞模式的套接字。

I/O多路技术一般在下面这些情况中被使用:
       当一个客户端需要同时处理多个文件描述符的输入输出操作的时候(一般来说是标准的输入输出和网络套接字), I/O 多路复用技术将会有机会得到使用。
       当程序需要同时进行多个套接字的操作的时候。
       如果一个TCP 服务器程序同时处理正在侦听网络连接的套接字和已经连接好的套接字。
       如果一个服务器程序同时使用TCP 和UDP 协议。
       如果一个服务器同时使用多种服务并且每种服务可能使用不同的协议(比如inetd就是这样的)。

I/O 多路服用技术并不只局限与网络程序应用上。几乎所有的程序都可以找到应用I/O多路复用的地方。

fcntl()函数 
     简单的说,阻塞就是"睡眠"的同义词.
     如你运行上面的listener 的时候,它只不过是简单的在那里等待接收数据。它调用recvfrom()函数,但是那个时候(listener 调用recvfrom()函数的时候),它并没有数据可以接收.所以recvfrom()函数阻塞在那里(也就是程序停在recvfrom()函数处睡大 觉)直到有数据传过来阻塞.你应该明白它的意思。

当你一开始建立一个套接字描述符的时候,系统内核就被设置为阻塞状态。如果你不想你的套接字描述符是处于阻塞状态的,那么你可以使用函数fcntl()。

#include <unistd.h>
     #include <fcntl.h>
     int fcntl (int fd, int cmd, long arg);

示例:
     sockfd = socket(AF_INET, SOCK_STREAM, 0);
     fcntl(sockfd, F_SETFL, O_NONBLOCK);

这样将一个套接字设置为无阻塞模式后,你可以对套接字描述符进行有效的“检测”.
     如果你尝试从一个没有接收到任何数据的无阻塞模式的套接字描述符那里读取数据,那么读取函数会马上返回–1 代表发生错误,全局变量errno 中的值为EWOULDBLOCK。
     一般来说,这种无阻塞模式在某些情况下不是一个好的选择。假如你的程序一直没有接收到传过来的数据,那么你的程序就会进行不停的循环来检查是否有数据到 来,浪费了大量的CPU 时间,而这些CPU 时间本来可以做其他事情的。
     另外一个比较好的检测套接字描述符的方法是调用select()函数

套接字选择项select()函数 
/*
*
*  这个技术有一点点奇怪但是它对我们的程序确是非常有用的。
*  假想一下下面的情况:
*      你写的服务器程序想监听客户端的连接,但是你同时又想从你以前已经建立过的连接中来读取数据。
*  你可能会说:“没有问题,我不就是需要使用一个accept()函数和一对儿recv()函数吗?”。
*  不要这么着急,你要想想,当你调用accept()函数阻塞的时候,你还能调用recv()函数吗?
*  “使用非阻塞套接字!”你可能会这么说。是的,你可以。但是如果你又不想浪费宝贵的CPU 时间,该怎么办呢?
*  Select()函数可以帮助你同时监视许多套接字。它会告诉你哪一个套接字已经可以读取数据,
*  哪个套接字已经可以写入数据,甚至你可以知道哪个套接字出现了错误,如果你想知道的话。

*  
*
* 当select()函数返回的时候,readfds 将会被修改用来告诉你哪一个文件描述符你可以用来读取数据。
*
* numfds         是readfds,writefds,exceptfds 中fd 集合中文件描述符中最大的数字加上1 也就是sockfd+1(因为标准输入的文件描述符的值为0 ,所以其他任何的文件描述符都会比标准输入的文件描述符大)。
*
* readfds        中的fd 集合将由select 来监视是否可以读取,如果你想知道是是否可以从标准输入和一些套接字(sockfd)中读取数据,你就可以把文件描述符和sockfd 加入readfds 中。
* writefds       中的fds 集合将由select 来监视是否可以写入
* exceptfds      中的fds 集合将由select 来监视是否有例外发生
* struct timeval 超时设置。 
*                    一般来说,如果没有任何文件描述符满足你的要求,你的程序是不想永远等下去的.也许每隔1 分钟你就想在屏幕上输出信息:“hello!”。
*                这个代表时间的结构将允许你定义一个超时。
*                在调用select()函数中,如果时间超过timeval 参数所代表的时间长度,
*                而还没有文件描述符满足你的要求,那么select()函数将回返回,允许你进行下面的操作。
*                只需要将tv_sec 设置为你想等待的秒数,然后设置tv_usec 为想等待的微秒数
*                (真正的时间就是tv_sec 所表示的秒数加上tv_usec 所表示的微秒数).注意,是微秒(百万分之一)而不是毫秒.
*                一秒有1,000 毫秒,一毫秒有1,000 微秒。所以,一秒有1,000,000 微秒. 
*                这个timeval 结构定义如下:
*                struct timeval
*                {
*                    int tv_sec ;   //秒数
*                    int tv_usec ;  //微秒
*                };
*                我们拥有了一个以微秒为单位的记时器!但是因为Linux 和UNIX 一样,最小的时间片是100 微秒,所以不管你将tv_usec 设置的多小,实质上记时器的最小单位是100微秒.

*                如果你将struct timeval 设置为0,则select()函数将会立即返回,同时返回在你的集合中的文件描述符的状态。 
*
*                如果你将timeout 这个参数设置为NULL,则select()函数进入阻塞状态,除了等待到文件描述符的状态变化,否则select()函数不会返回。
*
*
* return        当select()函数返回的时候,timeval 中的时间将会被设置为执行为select()后还剩下的时间。
*
*
*
*/
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

使用FD_ISSET() 宏,你可以选出select()函数执行的结果。 
在进行更深的操作 前,我们来看一看怎样处理这些fd_sets。下面这些宏可以是专门进行这类操作的:
  FD_ZERO(fd_set *set)           将一个文件描述符集合清零
  FD_SET(int fd, fd_set *set)    将文件描述符fd 加入集合set 中。
  FD_CLR(int fd, fd_set *set)    将文件描述符fd 从集合set 中删除.
  FD_ISSET(int fd, fd_set *set)  测试文件描述符fd 是否存在于文件描述符set 中.

下面这段代码演示了从标准输入等待输入等待2.5 秒.
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
/* 标准输入的文件描述符数值 */
#define STDIN 0
main()
{

fd_set readfds;

struct timeval tv;

/* 设置等待时间为2 秒零500,000 微秒 */
   tv.tv_sec  = 2;
   tv.tv_usec = 500000;

FD_ZERO(&readfds);
   FD_SET(STDIN, &readfds);

/* 因为我们只想等待输入,所以将writefds 和execeptfds 设为NULL */
   /* 程序将会在这里等待2 秒零500,000 微秒,除非在这段时间中标准输入有操作 */
   select(STDIN+1, &readfds, NULL, NULL, &tv);

/* 测试STDIN 是否在readfds 集合中 */
   if (FD_ISSET(STDIN, &readfds))
   {
      /* 在,则在标准输入有输入 */
      printf(“A key was pressed!/n”);
   }
   else
   {
      /* 不在,则在标准输入没有任何输入 */
      printf(“Timed out./n”);
   }
}

在标准输入上,你需要输入回车后终端才会将输入的信息传给你的程序。所以如果你没有输入回车的话,程序会一直等待到超时。
对 select()函数需要注意的最后一点:如果你的套接字描述符正在通过listen()函数侦听等待一个外来的网络连接,则你可以使用select() 函数(将套接字描述符加入readfds 集合中)来测试是否存在一个未经处理的新连接

上一篇:linux初级学习笔记二:linux操作系统及常用命令,文件的创建与删除和命名规则,命令行展开以及linux中部分目录的作用!(视频序号:02_3)


下一篇:ASP.NET获取服务器文件的物理路径