Socket 相关的知识

1、关于PF_INET和AF_INET的区别  

在写网络程序的时候,建立TCP socket:
 sock = socket(PF_INET, SOCK_STREAM, 0);
然后在绑定本地地址或连接远程地址时需要初始化sockaddr_in结构,其中指定address family时一般设置为AF_INET,即使用IP。

相关头文件中的定义:AF = Address Family
                            PF = Protocol Family
                            AF_INET = PF_INET

在windows中的Winsock2.h中,

                           #define AF_INET 0
                           #define PF_INET AF_INET

所以在windows中AF_INET与PF_INET完全一样.  

    而在Unix/Linux系统中,在不同的版本中这两者有微小差别.对于BSD,是AF,对于POSIX是PF.

    理论上建立socket时是指定协议,应该用PF_xxxx,设置地址时应该用AF_xxxx。当然AF_INET和PF_INET的值是相同的,混用也不会有太大的问题。也就是说你socket时候用PF_xxxx,设置的时候用AF_xxxx也是没关系的,这点随便找个TCPIP例子就可以验证出来了。如下,不论是AF_INET还是PF_INET都是可行的,只不过这样子的话,有点不符合规范。

 /* 服务器端开始建立socket描述符 */
// if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1) 
 if((sockfd=socket(PF_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_family=PF_INET;
 server_addr.sin_addr.s_addr=htonl(INADDR_ANY);
 server_addr.sin_port=htons(portnumber);
在函数socketpair与socket的domain参数中有AF_UNIX,AF_LOCAL,AF_INET,PF_UNIX,PF_LOCAL,PF_INET.
这几个参数有AF_UNIX=AF_LOCAL, PF_UNIX=PF_LOCAL, AF_LOCAL=PF_LOCAL, AF_INET=PF_INET.
但是对于socketpair与socket的domain参数,使用PF_LOCAL系列,
而在初始化套接口地址结构时,则使用AF_LOCAL.
例如:
     z = socket(PF_LOCAL, SOCK_STREAM, 0);
     adr_unix.sin_family = AF_LOCAL;
Linux 下的address family定义
/* Supported address families. */
#define AF_UNSPEC    0
#define AF_UNIX        1    /* Unix domain sockets         */
#define AF_LOCAL    1    /* POSIX name for AF_UNIX    */
#define AF_INET        2    /* Internet IP Protocol     */
#define AF_AX25        3    /* Amateur Radio AX.25         */
#define AF_IPX        4    /* Novell IPX             */
#define AF_APPLETALK    5    /* AppleTalk DDP         */
#define AF_NETROM    6    /* Amateur Radio NET/ROM     */
#define AF_BRIDGE    7    /* Multiprotocol bridge     */
#define AF_ATMPVC    8    /* ATM PVCs            */
#define AF_X25        9    /* Reserved for X.25 project     */
#define AF_INET6    10    /* IP version 6            */
#define AF_ROSE        11    /* Amateur Radio X.25 PLP    */
#define AF_DECnet    12    /* Reserved for DECnet project    */
#define AF_NETBEUI    13    /* Reserved for 802.2LLC project*/
#define AF_SECURITY    14    /* Security callback pseudo AF */
#define AF_KEY        15      /* PF_KEY key management API */
#define AF_NETLINK    16
#define AF_ROUTE    AF_NETLINK /* Alias to emulate 4.4BSD */
#define AF_PACKET    17    /* Packet family        */
#define AF_ASH        18    /* Ash                */
#define AF_ECONET    19    /* Acorn Econet            */
#define AF_ATMSVC    20    /* ATM SVCs            */
#define AF_RDS        21    /* RDS sockets             */
#define AF_SNA        22    /* Linux SNA Project (nutters!) */
#define AF_IRDA        23    /* IRDA sockets            */
#define AF_PPPOX    24    /* PPPoX sockets        */
#define AF_WANPIPE    25    /* Wanpipe API Sockets */
#define AF_LLC        26    /* Linux LLC            */
#define AF_CAN        29    /* Controller Area Network      */
#define AF_TIPC        30    /* TIPC sockets            */
#define AF_BLUETOOTH    31    /* Bluetooth sockets         */
#define AF_IUCV        32    /* IUCV sockets            */
#define AF_RXRPC    33    /* RxRPC sockets         */
#define AF_ISDN        34    /* mISDN sockets         */
#define AF_PHONET    35    /* Phonet sockets        */
#define AF_IEEE802154    36    /* IEEE802154 sockets        */
#define AF_CAIF        37    /* CAIF sockets            */
#define AF_MAX        38    /* For now.. */

/* Protocol families, same as address families. */
#define PF_UNSPEC    AF_UNSPEC
#define PF_UNIX        AF_UNIX
#define PF_LOCAL    AF_LOCAL
#define PF_INET        AF_INET
#define PF_AX25        AF_AX25
#define PF_IPX        AF_IPX
#define PF_APPLETALK    AF_APPLETALK
#define PF_NETROM    AF_NETROM
#define PF_BRIDGE    AF_BRIDGE
#define PF_ATMPVC    AF_ATMPVC
#define PF_X25        AF_X25
#define PF_INET6            AF_INET6
#define PF_ROSE        AF_ROSE
#define PF_DECnet    AF_DECnet
#define PF_NETBEUI    AF_NETBEUI
#define PF_SECURITY    AF_SECURITY
#define PF_KEY        AF_KEY
#define PF_NETLINK    AF_NETLINK
#define PF_ROUTE    AF_ROUTE
#define PF_PACKET    AF_PACKET
#define PF_ASH        AF_ASH
#define PF_ECONET    AF_ECONET
#define PF_ATMSVC    AF_ATMSVC
#define PF_RDS        AF_RDS
#define PF_SNA        AF_SNA
#define PF_IRDA        AF_IRDA
#define PF_PPPOX            AF_PPPOX
#define PF_WANPIPE    AF_WANPIPE
#define PF_LLC        AF_LLC
#define PF_CAN        AF_CAN
#define PF_TIPC        AF_TIPC
#define PF_BLUETOOTH    AF_BLUETOOTH
#define PF_IUCV        AF_IUCV
#define PF_RXRPC    AF_RXRPC
#define PF_ISDN        AF_ISDN
#define PF_PHONET    AF_PHONET
#define PF_IEEE802154    AF_IEEE802154
#define PF_CAIF        AF_CAIF
#define PF_MAX        AF_MAX

2、linux之shutdown()与close()函数

1.close()函数

int close(int sockfd);     //返回成功为0,出错为-1.

close 一个套接字的默认行为是把套接字标记为已关闭,然后立即返回到调用进程,该套接字描述符不能再由调用进程使用,也就是说它不能再作为readwrite的第一个参数,然而TCP将尝试发送已排队等待发送到对端的任何数据,发送完毕后发生的是正常的TCP连接终止序列。

    在多进程并发服务器中,父子进程共享着套接字,套接字描述符引用计数记录着共享着的进程个数,当父进程或某一子进程close掉套接字时,描述符引用计数会相应的减一,当引用计数仍大于零时,这个close调用就不会引发TCP的四路握手断连过程。

2.shutdown()函数

int shutdown(int sockfd,int howto);  //返回成功为0,出错为-1.

该函数的行为依赖于howto的值

    1.SHUT_RD:值为0,关闭连接的读这一半。

    2.SHUT_WR:值为1,关闭连接的写这一半。

    3.SHUT_RDWR:值为2,连接的读和写都关闭。

    终止网络连接的通用方法是调用close函数。但使用shutdown能更好的控制断连过程(使用第二个参数)。

3.两函数的区别

    closeshutdown的区别主要表现在:

    close函数会关闭套接字ID,如果有其他的进程共享着这个套接字,那么它仍然是打开的,这个连接仍然可以用来读和写,并且有时候这是非常重要的,特别是对于多进程并发服务器来说。

   shutdown会切断进程共享的套接字的所有连接,不管这个套接字的引用计数是否为零,那些试图读得进程将会接收到EOF标识,那些试图写的进程将会检测到SIGPIPE信号,同时可利用shutdown的第二个参数选择断连的方式。

    下面将展示一个客户端例子片段来说明使用closeshutdown所带来的不同结果:

     客户端有两个进程,父进程和子进程,子进程是在父进程和服务器建连之后fork出来的,子进程发送标准输入终端键盘输入数据到服务器端,知道接收到EOF标识,父进程则接受来自服务器端的响应数据。

   s=connect(...); 

   if( fork() ){    
       while( gets(buffer) >0) 
           write(s,buf,strlen(buffer)); 
close(s); exit(
0); } else { while( (n=read(s,buffer,sizeof(buffer)){ do_something(n,buffer);
wait(
0); exit(0); }

对于这段代码,我们所期望的是子进程获取完标准终端的数据,写入套接字后close套接字,并退出,服务器端接收完数据检测到EOF(表示数据已发送完),也关闭连接,并退出。接着父进程读取完服务器端响应的数据,并退出。然而,事实会是这样子的嘛,其实不然!子进程close套接字后,套接字对于父进程来说仍然是可读和可写的,尽管父进程永远都不会写入数据。因此,此socket的断连过程没有发生,因此,服务器端就不会检测到EOF标识,会一直等待从客户端来的数据。而此时父进程也不会检测到服务器端发来的EOF标识。这样服务器端和客户端陷入了死锁(deadlock)。如果用shutdown代替close,则会避免死锁的发生。

if( fork() ) {   
      while( gets(buffer) 
         write(s,buffer,strlen(buffer)); 

      shutdown(s,1);  
      exit(0); 
 } 

 3、当客户端保持着与服务器端的连接,这时服务器端断开,再开启服务器时会出现: Address already in usr
      可以用netstat -anp | more 可以看到客户端还保持着与服务器的连接(还在使用服务器bind的端口)。这是由于client没有执行close,连接还会等待client的FIN包一段时间。解决方法是使用setsockopt,使得socket可以被重用,是最常用的服务器编程要点。具体的做法为是,在socket调用和bind调用之间加上一段对socket的设置:

int opt = 1;
setsockopt(socket_fd,SOL_SOCKET,SO_REUSEADDR,
&opt,sizeof(opt));

4、 指定网卡IP信息设置

/*----------------------------------------------------------------------------
 Network Info
-----------------------------------------------------------------------------*/
static int gateway_info(char *dev, char *gateway, int set)
{
    FILE *fp;
    unsigned char buf[128], gate[16];
    unsigned char *find;

    //# get gateway
    sprintf(buf, "route -n | grep 'UG[ \t]' | grep %s | awk '{print $2}'", dev);

    fp = popen(buf, "r");
    if(NULL == fp) {
        eprintf("popen error (%s)\n", buf);
        return -1;
    }

    if(!fgets(gate, 16, fp))    {
        strcpy(gate, "0.0.0.0");
    }
    else {
        find = strchr(gate,'\n');    //# remove '\n'
        if(find) *find='\0';
    }
    pclose(fp);

    if(set)    //# set gateway
    {
        if(strcmp(gate, "0.0.0.0")) {
            sprintf(buf, "route del default gw %s %s", gate, dev);
            system_user(buf);
        }
        sprintf(buf, "route add default gw %s %s", gateway, dev);
        system_user(buf);
    }
    else
    {
        strcpy(gateway, gate);
    }

    return 0;
}

int get_net_info(int devno, dvr_net_info_t *inet)
{
    int ret, fd;
    char dev[8];
    struct ifreq ifr;

    fd = socket(AF_INET, SOCK_DGRAM, 0);

    /* I want to get an IPv4 IP address */
    ifr.ifr_addr.sa_family = AF_INET;

    /* I want IP address attached to "eth0" */
    sprintf(dev, "eth%d", devno);
    strncpy(ifr.ifr_name, dev, IFNAMSIZ-1);

    //# check up/down
    ioctl(fd, SIOCGIFFLAGS, &ifr);
    inet->state = ifr.ifr_flags & IFF_UP;

    #if 1
    if(!inet->state) {    //# down
        close(fd);
        strcpy(inet->ip, "0.0.0.0");
        strcpy(inet->mask, "255.255.255.0");
        strcpy(inet->gate, "0.0.0.0");
        return 0;
    }
    #endif

    ret = ioctl(fd, SIOCGIFADDR, &ifr);
    if(ret<0)    
        strcpy(inet->ip, "0.0.0.0");
    else    
        sprintf(inet->ip, "%s", inet_ntoa(((struct sockaddr_in *)&ifr.ifr_addr)->sin_addr));

    ret = ioctl(fd, SIOCGIFNETMASK, &ifr);
    if(ret<0)    
        strcpy(inet->mask, "255.255.255.0");
    else    
        sprintf(inet->mask, "%s", inet_ntoa(((struct sockaddr_in *)&ifr.ifr_addr)->sin_addr));

    close(fd);

    gateway_info(dev, inet->gate, 0);

    return 0;
}

int set_net_info(int devno, dvr_net_info_t *inet)
{
        int ret, fd;
        char dev[8], cmd[32];
        struct ifreq ifr;
        struct sockaddr_in sin;

        if(!strcmp(inet->ip, "0.0.0.0"))
                return -1;

        sprintf(dev, "eth%d", devno);

        fd = socket(AF_INET, SOCK_DGRAM, 0);
        strncpy(ifr.ifr_name, dev, IFNAMSIZ);

        //# check up/down
        ioctl(fd, SIOCGIFFLAGS, &ifr);
        inet->state = ifr.ifr_flags & IFF_UP;
        if(!inet->state) {    //# down
                ifr.ifr_flags |= IFF_UP;
                ioctl(fd, SIOCSIFFLAGS, &ifr);
                sleep(1);
        }

        if(inet->type == NET_DHCP)
        {
                sprintf(cmd, "udhcpc -n -i %s", dev);
                ret = system_user(cmd);
        }
        else if(inet->type == NET_STATIC)
        {
                //memset(&sin, 0, sizeof(struct sockaddr));
                sin.sin_family = AF_INET;
                sin.sin_addr.s_addr = inet_addr(inet->ip);
                memcpy(&ifr.ifr_addr, &sin, sizeof(struct sockaddr));
                ioctl(fd, SIOCSIFADDR, &ifr);

                sin.sin_addr.s_addr = inet_addr(inet->mask);
                memcpy(&ifr.ifr_addr, &sin, sizeof(struct sockaddr));
                ret = ioctl(fd, SIOCSIFNETMASK, &ifr);
                if(ret < 0)
                        dprintf("netmask: Invalid argument\n");

                gateway_info(dev, inet->gate, 1);
        }

        close(fd);

        return ret;
}

5、recv和recvform

  recv和recvfrom都可用TCP或者UDP,只是习惯性TCP用recv,因为基于连接的对方socket是已知的,UDP用recvfrom,因为一般用没有bind远端socket,接受到本地端口的所有数据,需要recvfrom识别远端地址。

  recv的recvfrom是可以替换使用的,只是recvfrom多了两个参数,可以用来接收对端的地址信息,这个对于udp这种无连接的,可以很方便地进行回复。而换过来如果你在udp当中也使用recv,那么就不知道该回复给谁了,如果你不需要回复的话,也是可以使用的。另外就是对于tcp是已经知道对端的,就没必要每次接收还多收一个地址,没有意义,要取地址信息,在accept当中取得就可以加以记录了

 6、gethostname和gethostbyname

  gethostname() -- 获取进程所在机器的计算机的名字。
  gethostbyname() -- 用域名或主机名获取IP地址,这个域名或主机名可以是本地机器的主机名/域名;也可以是远端节点的域名。

 7、Linux下端口复用(SO_REUSEADDR与SO_REUSEPORT) 

  只考虑AF_INET的情况(同一端口指ip地址与端口号都相同)
  1.freebsd支持SO_REUSEPORT和SO_REUSEADDR选项,而linux只支持SO_REUSEADDR选项。
  2.freebsd下,使用SO_REUSEPORT选项,两个tcp的socket可以绑定同一个端口;同样,使用SO_REUSEPORT选项,两个udp的socket可以绑定同一个端口。
  3.linux下,两个tcp的socket不能绑定同一个端口;而如果使用SO_REUSEADDR选项,两个udp的socket可以绑定同一个端口。
  4.freebsd下,两个tcp的socket绑定同一端口,只有第一个socket获得数据。
  5.freebsd下,两个udp的socket绑定同一端口,如果数据包的目的地址是单播地址,则只有第一个socket获得数据,而如果数据包的目的地址是多播地址,则两个socket同时获得相同的数据。
  6.linux下,两个udp的socket绑定同一端口,如果数据包的目的地址是单播地址,则只有最后一个socket获得数据,而如果数据包的目的地址是多播地址,则两个socket同时获得相同的数据。

  SO_REUSEADDR提供如下四个功能:
  1.SO_REUSEADDR允许启动一个监听服务器并捆绑其众所周知端口,即使以前建立的将此端口用做他们的本地端口的连接仍存在。这通常是重启监听服务器时出现,若不设置此选项,则bind时将出错。 
  2.SO_REUSEADDR允许在同一端口上启动同一服务器的多个实例,只要每个实例捆绑一个不同的本地IP地址即可。对于TCP,我们根本不可能启动捆绑相同IP地址和相同端口号的多个服务器。 
  3.SO_REUSEADDR允许单个进程捆绑同一端口到多个套接口上,只要每个捆绑指定不同的本地IP地址即可。这一般不用于TCP服务器。 
  4.SO_REUSEADDR允许完全重复的捆绑:当一个IP地址和端口绑定到某个套接口上时,还允许此IP地址和端口捆绑到另一个套接口上。一般来说,这个特性仅在支持多播的系统上才有,而且只对UDP套接口而言(TCP不支持多播)。 

  SO_REUSEPORT选项有如下语义:
  此选项允许完全重复捆绑,但仅在想捆绑相同IP地址和端口的套接口都指定了此套接口选项才性。 
  如果被捆绑的IP地址是一个多播地址,则SO_REUSEADDR和SO_REUSEPORT等效。 

  使用这两个套接口选项的建议
  在所有TCP服务器中,在调用bind之前设置SO_REUSEADDR套接口选项; 
  当编写一个同一时刻在同一主机上可运行多次的多播应用程序时,设置SO_REUSEADDR选项,并将本组的多播地址作为本地IP地址捆绑。

8、UDP 调用 connect的作用 

  1.UDP中可以使用connect系统调用

  2.UDP中connect操作与TCP中connect操作有着本质区别。
    TCP中调用connect会引起三次握手,client与server建立连结.UDP中调用connect内核仅仅把对端ip&port记录下来。

  3.UDP中可以多次调用connect,TCP只能调用一次connect。
  UDP多次调用connect有两种用途:
    1,指定一个新的ip&port连结,指定新连结,直接设置connect第二个参数即可。
    2,断开和之前的ip&port的连结,断开连结,需要将connect第二个参数中的sin_family设置成 AF_UNSPEC即可。

  4.UDP中使用connect可以提高效率.原因如下:
  普通的UDP发送两个报文内核做了如下:
    #1:建立连结
    #2:发送报文
    #3:断开连结
    #4:建立连结
    #5:发送报文
    #6:断开连结
  采用connect方式的UDP发送两个报文内核如下处理:
    #1:建立连结
    #2:发送报文
    #3:发送报文另外一点, 每次发送报文内核都由可能要做路由查询。

  5.采用connect的UDP发送接受报文可以调用send,write和recv,read操作.当然也可以调用sendto,recvfrom.
  调用sendto的时候第五个参数必须是NULL,第六个参数是0,调用recvfrom,recv,read系统调用只能获取到先前connect的ip&port发送的报文。

  6.UDP中使用connect的好处:
  1:会提升效率.前面已经描述了.
  2:高并发服务中会增加系统稳定性.原因:
  假设client A 通过非connect的UDP与server B,C通信.B,C提供相同服务。为了负载均衡,我们让A与B,C交替通信。A 与 B通信IPa:PORTa <----> IPb:PORTb;A与 C通信IPa:PORTa' <---->IPc:PORTc 。假设PORTa与PORTa'相同了(在大并发情况下会发生这种情况),那么就有可能出现A等待B的报文,却收到了C的报文.导致收报错误.解决方法内就是采用connect的UDP通信方式.在A中创建两个udp,然后分别connect到B,C。

9、getsockname与getpeername

  getsockname()是返回套接口关联的本地协议地址。
  getpeername()是返回套接口关联的远程协议地址。

  getsockname和getpeername调度时机很重要,如果调用时机不对,则无法正确获得地址和端口。
  TCP:
  1>对于服务器来说,在bind以后就可以调用getsockname来获取本地地址和端口,虽然这没有什么太多的意义。getpeername只有在链接建立以后才调用,否则不能正确获得对方地址和端口,所以他的参数描述字一般是链接描述字而非监听套接口描述字。
  2>对于客户端来说,在调用socket时候内核还不会分配IP和端口,此时调用getsockname不会获得正确的端口和地址(当然链接没建立更不可能调用getpeername),当然如果调用了bind 以后可以使用getsockname。想要正确的到对方地址(一般客户端不需要这个功能),则必须在链接建立以后,同样链接建立以后,此时客户端地址和端口就已经被指定,此时是调用getsockname的时机。
  UDP:
  UDP分为链接和没有链接2种:
  1>没有链接的UDP不能调用getpeername,但是可以调用getsockname,和TCP一样,他的地址和端口不是在调用socket就指定了,而是在第一次调用sendto函数以后
  2>已经链接的UDP,在调用connect以后,这2个函数都是可以用的(同样,getpeername也没太大意义。如果你不知道对方的地址和端口,不可能会调用connect)。

10、用域名取得主机的ip地址(gethostbyname)

  使用gethostbyname()函数包含2个头文件:

 #include <netdb.h>
 #include <sys/socket.h>

  gethostbyname()函数定义:

struct hostent *gethostbyname(const char *name);

  这个函数的传入值是域名或者主机名,例如"www.google.com","wpc"等等;传出值,是一个hostent的结构(如下)。如果函数调用失败,将返回NULL

  hostent结构体定义:

struct hostent {
     char *h_name; //表示的是主机的规范名。例如www.google.com的规范名其实是www.l.google.com。
     char **h_aliases; // 表示的是主机的别名。www.google.com就是google他自己的别名。有的时候,有的主机可能有好几个别名,这些,其实都是为了易于用户记忆而为自己的网站多取的名字。
     int h_addrtype; // 表示的是主机ip地址的类型,到底是ipv4(AF_INET),还是ipv6(AF_INET6)
     int h_length; //表示的是主机ip地址的长度
     char **h_addr_list; //表示的是主机的ip地址,注意,这个是以网络字节序存储的。千万不要直接用printf带%s参数来打这个东西,会有问题的哇。所以到真正需要打印出这个IP的话,需要调用inet_ntop()。
};

  inet_ntop()函数定义:

const char *inet_ntop(int af, const void *src, char *dst, socklen_t cnt) :

  这个函数,是将类型为af的网络地址结构src,转换成主机序的字符串形式,存放在长度为cnt的字符串中。
  这个函数,其实就是返回指向dst的一个指针。如果函数调用错误,返回值是NULL。

  下面是例程,就是通过给定一个主机名,然后调用gethostbyname(hostname),返回一个struct hostent类型的数据结构其中包含 char **h_addr_list(为ip地址列表),然后调用inet_ntoa(struct in_addr in)打印出ip地址。

#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<netdb.h>

main(int argc,const char **argv)
{
    long addr;
    struct hostent *hp;
    char **p;

    hp=gethostbyname(argv[1]); /* 调用gethostbyname()。调用结果都存在hp中 注意有的获取IP地址的时候,需要去掉“://”前面的和“/”后面的*/
    if(hp==NULL)
    {
         (void)printf("host information for %s not found\n",argv[1]);
         exit(2);
    }
for(p=hp->h_addr_list;*p!=0;p++) { struct in_addr in; char **q;
memcpy(&in.s_addr,*p,sizeof(in.s_addr));
printf("%s\t%s",inet_ntoa(in),hp->h_name);/* 将刚才得到的所有地址都打出来。其中调用了inet_ntoa()函数 */ for(q=hp->h_aliases;*q!=0;q++) printf("%s",*q); putchar('\n'); } exit(0); }

 11.udp数据发送

  udp数据发送的目标地址,一个固定值,一个接受到的地址,如果经过了路由器,那么这个接受地址变成了路由器地址,再次发送就不是已经的目标地址,而tcp的面向对象连接,建立了链路,所以远程的时候或者访问上级子网的时候,要么采取tcp的,要么采取udp 固定ip地址发送。

posted on 2014-04-09 11:31  CSlunatic  阅读(938)  评论(0编辑  收藏  举报

导航