网络编程总结


基本知识:
1.协议
【作用】:使交换信息的两个部分之间相互理解的一组规则、
约定和数据结构的集合。
即:为了使不同版本的计算机能相互沟通而存在
在现在的网络应用中使用最广泛的协议是TCP/IP协议

ISO
OSI【【七层协议】】模型
【1】.应用层:为应用(程序提供服务)并(规定)应用程序中(通信的相关细节),
如:ftp,tftp,smtp,ssh,telnet...
【2】.表示层:主要负责数据格式的转换(设备固有格式     <==>     网络格式)
【3】.会话层:主要建立和断开通信连接
【4】.传输层:起着可靠传输的作用
【5】.网络层:将数据传输到目标地址,主要负责寻址和路由选择
【6】.数据链路层:负责物理层面上的互连的节点之间的通信传输
【7】.物理层:负责0、1比特流与电压的高低、光的闪灭之间的互换


根据七层协议模型,    tcp/IP协议可分为四层
【1】.应用层:
【2】.传输层:        TCP        UDP
【3】.网络层:        IPv4        IPv6
【4】.网络接口层

2.常用协议
【1】.TCP协议
        传输控制协议,是一种【面向连接】的协议,类似打电话
【2】.UDP协议
        用户数据报协议,是一种【无连接】的协议,类似发短信
【3】.IP协议
        互联网协议,是上述两种协议的底层协议,当需要【开发
        新的通信协议】时,才需要关注

3.IP地址
【作用】:作为通信设备在互联网中的唯一地址标识
【本质】:            1.由32位二进制组成的【整数】(IPv4)
                        2.由128位二进制组成的【整数】(IPv6)

日常生活中采用点分十进制表示法来描述IP地址,也就是将每个字节的
二进制转为十进制的整数,不同的整数之间用小数点分隔
    0x01020304        ==>1.2.3.4

    2^32个地址,怎么管理?
    将【IP地址分为两部分】:网络地址和主机地址
    【网络地址】:属于哪个网络            【可以定位在哪个网吧上网】
    【主机地址】:网络中主机的编号    【可以定位在哪台电脑上网】                    
                        
    根据网络地址和主机地址位数的不同分为四类:
    A:    0 + 7位的网络号 +     24位主机地址
        0.0.0.0   ~        127.255.255.255 
    B:    10 + 14位网络号    +    16位主机地址
        128.0.0.0 ~        191.255.255.255 
    C:    110 + 21位网络号    +    8位主机地址
        192.0.0.0 ~        223.255.255.255 
    D:    1110 + 28位多(组)播地址
        224.0.0.0 ~        239.255.255.255
    E:    备用
    
    查看IP地址的命令
    ipconfig
    ifconfig                    
                        
3.子网掩码        【第一种分IP地址的方法】
【作用】:用于划分IP地址中的网络地址和主机地址,
                也可以用于判断两个IP是否在同一局域网中
    具体分法:        【IP地址】    跟 【子网掩码】     做【与运算】    得到 【网络地址】
    【前提是换成二进制】
    IP地址 & 子网掩码 = 网络地址
    例:
        172.30.100.64        IP
    &    255.255.255.0         子网掩码
    --------------------------------
        172.30.100.0 ---网络地址
                   64---主机地址
        
    
    例:
        IP:166.111.160.1 和    166.111.161.45
  子网掩码:255.255.254.0 
    解析:
        166.111.160.1 
        255.255.254.0 &
        ---------------
        166.111.160    网络号

        166.111.161.45 
        255.255.254.0 &
        ---------------
        166.111.160 网络号
        
    总结:上面两个IP地址在同一个局域网中
【第二种分IP地址的方法】:    
    斜杠'/'后面的数字N表示前N位为网络号
    166.111.160.1     /23     
    166.111.161.45    /23     
    23:表示前23位为网络号    
                            
4.端口号
    【IP地址】 -- 定位到具体的某一台主机/设备            【推断出你在哪个网吧哪台机子上网】
    【端口号】 -- 定位到主机/设备上的某一个进程            【推断出你在那台机子玩什么】
    本质上就是一个16位的无符号整数    unsigned short,范围是0~65535
    其中0-1024之间的端口号被系统使用,建议【从5000开始使用】
                        
    【网络编程】中需【要提供】两个信息:【IP地址】+【端口号】                     
                        
5.字节序(多字节整数)
    小端模式:主要指将低位字节数据保存在低位内存地址的系统
    大端模式:主要指将低位字节数据保存在高位内存地址的系统
    【在准备通信地址和使用通信地址信息时需要注意类型的转换】                    
                        
----------------------------------------------------------                        
网络编程【又名socket编程,因为要使用socket函数得到信息载体】                        
一、网络编程又叫socket编程
【套接字概念】:是一个网络编程的接口,是网络数据传输的软设备。
【套接字作用】:用于网络交互。

【网络编程本质】:编写程序【使两台连网的计算机】相互【交换数据】。
    
unix/linux系统作为服务器操作系统存在至今,因此,
Linux的网络功能应该是非常全面和强大。    
网络编程其实有很成熟的通信模型,并且windows也通用。
                        
二、通信模型(基于TCP的一对一的通信模型)                        
【1】.一对一服务器:

【等待连接原理】:
1 有人从很远很远的地方尝试调用 connect()来连接你的机器上的某个端口
(当然是你已经在 listen()的)。
2 他的连接将被 listen 加入等待队列等待 accept()函数的调用(加入等待队
列的最多数目由调用 listen()函数的第二个参数 backlog 来决定)。
3 你调用 accept()函数,告诉他你准备连接。
4 accept()函数将返回一个新的套接字描述符,这个描述符就代表了这个连接!

好,这时候你有了【两个套接字描述符】,返回给你的那个就是和远程计算机的连接,
而第一个套接字描述符仍然在你的机器上原来的那个端口上 listen()。
这时候你所得到的那个新的套接字描述符就可以进行 send()操作和recv()操作了。    

    http://blog.csdn.net/wukui1008/article/details/7669173    【套接字】    
        【监听套接字】:创建通信载体时得到的返回值;
        作用:用来监听一个端口,当有客户端过来时,给他指路
        【连接套接字】:等待客户端连接完成后得到的返回值;
        作用:作为和客户端(远程计算机)真正联系的【链接】
        
        监听套接字就是个牵线指路的,你实质上是跟它指的那个人说话。
        因为你要找的那个人不可能随时等你来,而监听套接字就是专职等你来问,
        它回答你要找的人在哪,并唤醒你要找的人,于是通话就建立起来了,
        就像现实生活中的接线员一样。
                    
1.创建监听套接字    使用socket()函数
    原型:int socket(int domain, int type, int protocol);
    即:socket(协议族,通信类型,具体的协议);
    功能:
            主要用于创建可以实现通信的交流点,也就是socket通信载体(相当于电话机)
    得到监听套接字        
    返回值:
            成功:返回新的socket描述符【监听套接字】
            失败:返回-1,errno被设置            
    例如:
            int sockfd = socket(AF_INET,SOCK_DGRAM,0);
        AF_INET:域/协议族,决定了是【本地通信还是网络通信】
                    AF_UNIX/AF_LOCAL    用于实现本地通信
                    AF_INET                用于实现基于IPv4的网络通信
                    AF_INET6            用于实现基于IPv6的网络通信
        SOCK_DGRAM:通信类型,决定了【具体的通信式】
                    SOCK_STREAM        提供有序的、可靠的、双向的、面向连接的字节流通信方式,默认使用TCP协议
                    SOCK_DGRAM        提供无序的、不可靠的,非面向连接的数据报通信方式,默认使用UDP协议
        0:指定具体的协议,默认为0,使用默认协议
                        
2.准备通信地址【要先清零再赋值】                        
通信地址数据【类型】:
1】.通用地址结构
struct sockaddr
{
    sa_family_t sa_family;    //域/协议族
    char sa_data[14];        //??????
};

        2】.IPv4通信地址结构
        struct sockaddr_in
        {
            sa_family_t sin_family;//协议族,AF_INET
            in_port_t sin_port;//16位的端口号
            struct in_addr sin_addr;//IP地址 
        };
        struct in_addr
        {
            in_addr_t s_addr;    //16位的端口号
        };        
    例如:(IPv4通信地址结构)
    struct sockaddr_in saddr;
    memset(&saddr,0,sizeof(saddr));
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(atoi(argv[2]));
    saddr.sin_addr.s_addr = inet_addr(argv[1]);
    
【作用】:(以IPv4通信地址结构为例)                        
        saddr.sin_family:决定该地址遵循何种协议                
        saddr.sin_port:决定该地址的端口    【哪台机子】                
        saddr.sin_addr.s_addr:决定该地址的IP地址    【哪个网吧】                
【IP地址格式转换】:
        IP地址从main()函数【输入时是字符串】格式的,
        初始化通信地址的时候要用inet_addr()函数转换成结构体类型;
        接收【调用】别人的IP地址时,得到的是【结构体类型】的;
        若要【输出】,则要用inet_ntoa()函数转换成【字符串】格式;
        
【端口号格式转换】:
        端口号从main()函数【输入时是字符串】格式的,
        初始化通信地址的时候要先用atoi()函数转换成int型;
        是主机字节顺序,要再用 htons()函数转换成网络字节顺序(大端模式);
        接收【调用】别人的IP地址时,得到的是【网络字节顺序】;                                                                            
        若要【输出】,则要用ntohs()函数转换成【主机字节顺序】;                                                                            
    端口格式转换相关【函数详解】:
            #include

           uint32_t htonl(uint32_t hostlong);

           uint16_t htons(uint16_t hostshort);

           uint32_t ntohl(uint32_t netlong);

           uint16_t ntohs(uint16_t netshort);

        h:host 本地格式
        n:network    网络格式(大端模式)
        s:short        2字节整数
        l:long        4字节整数    
        功能:把本地格式转成网络格式,或者反过来。
        
        IP地址格式转换相关函数:
            in_addr_t inet_addr(const char *cp);
            功能:将字符串形式的IP地址转为整数类型
            char *inet_ntoa(struct in_addr in);
            功能:将结构类型的IP地址转为字符串形式
    
/*************服务器**************/    
3.绑定(通信地址与监听套接字)        bind()函数                    
        原型:int bind(int sockfd, const struct sockaddr *saddr,
                    socklen_t addrlen);                
        即:bind(监听套接字描述符,强转后的通信地址,通信地址大小)    ;            
        功能:
                主要用于绑定socket和具体的通信地址                
        返回值:
                成功:返回0
                失败:返回-1,errno被设置                
        例如:                
    int ret = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
    if(ret < 0)        //很有必要!!
    {
        perror("bind");
        exit(-1);
    }            
        sockfd:socket描述符,即监听套接字                
        (struct sockaddr*)&saddr:结构体指针,
        不管是什么协议的地址结构,都需要强转为该类型                
        sizeof(saddr):通信地址结构的大小,使用sizeof()计算即可                
                        
4.监听        使用listen()函数
        原型:int listen(int sockfd, int backlog);
        即:listen(监听套接字,监听队列大小);
        功能:
                为套接字sockfd建立一个连接请求监听队列,在调用listen函数成功后,这个套接字便成为服务套接字,即被动套接字        
        返回值:
                成功:返回0
                失败:返回-1,errno被设置
        例如:        
                listen(sockfd,10);
        sockfd:socket描述符,即监听套接字                                
        10:监听队列的大小                            

5.等待连接    accept()函数
        原型:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
        即:accept(监听套接字,强转后的通信地址,指针类型的通信地址大小);                                
        功能:
                用于接收服务套接字sockfd上的连接请求                
        返回值:
                成功:返回一个【连接套接字】
                (新的,在后面的代码中起作用,代替监听套接字出现)
                失败返回-1,errno被设置                        
        例如:                                
                socklen_t len = sizeof(caddr);        //不能直接在等待中用sizeof(caddr)
                //保存客户地址的结构体的大小,必需由调用者初始化    
                int confd = accept(sockfd,(struct sockaddr*)&caddr,&len);    
                caddr:    结构体指针,用于保存接受的客户端通信地址,
                            与被初始化的通信地址一起定义,用来存储客户端的地址
                sockfd:socket函数的返回值,监听套接字
                (struct sockaddr*)&caddr:结构体指针,
                    不管是什么协议的地址结构,都需要强转为该类型                                        
                &len:指针类型,用于保存客户端网络地址的信息的长度                            
        注:如果对客户地址不感兴趣,那么第二、三两个参数写NULL即可。
                如果需要客户的地址,那么第三个参数必须由调用者初始化。
                
6.通信    
【方法】:把连接套接字当成文件描述符操作,
可以读取里面的东西,也可以往里面写东西            

7.关闭连接套接字和监听套接字
            close(confd);
            close(sockfd);                            
/*********************服务器*************/    

/*****************客户端*************/    
3.连接        使用connect()函数
        原型:int connect(int sockfd, const struct sockaddr *addr,
                   socklen_t addrlen);                                        
        即:connect(监听套接字描述符,服务器的通信地址,通信地址结构的大小);                                        
        功能:
                用于连接服务器                                    
        返回值:
                成功:返回0
                失败:返回-1,errno被设置                                            
        例如:
            int ret = connect(sockfd,(struct sockaddr *)&saddr,sizeof(saddr));
            if(ret < 0)
            {
                perror("connect");
                exit(-1);
            }        

4.通信    
【方法】:把监听套接字当成文件描述符操作,
可以读取里面的东西,也可以往里面写东西    

5.关闭监听套接字
            close(sockfd);    
/****************客户端*************/                            
                        
迭代服务器

并发服务器
多进程服务器
多线程服务器
多路复用服务器
                        
【2】.IO多路复用服务器                        

【作用】:实现多个文件的查询是否就绪,
在某个文件可读的情况下,马上读到数据,                        
【方法】:运用select()函数                        
【原理】:对集合中的文件进行轮询,每隔一段时间就会去
            询问文件描述符[0,nfds)中的每一个文件,查看是否就绪,
            如果就绪那么把相应的文件描述符集合中置1.    .......
        直到有文件就绪或超时或出错。
    
    超时时间结构体:
        struct timeval {
               time_t         tv_sec;     /* 秒 */
               suseconds_t    tv_usec;    /* 毫秒 */
           };    
    例如:
            struct timeval tv;//超时时间结构体
           /* 超时时间设为5秒. */
            tv.tv_sec = 5;
            tv.tv_usec = 0;
            
select()【函数原型】:
                    int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);
    即:select(感兴趣的文件集合中最大的文件描述符,
                    对读感兴趣的文件描述符集合,
                    对写感兴趣的文件描述符集合,
                    对异常感兴趣的文件描述符集合,
                    超时时间);                
    返回值:
        1.有文件数据就绪,立马返回,返回就绪的文件描述符个数  >0 
        2.超时返回,返回0
        3.出错了,返回-1 ,errno被设置                
    例如:    
            int retval;
            retval = select(10, &tmpfds, NULL, NULL, &tv);                        
            if(retavl < 0)
            {    //出错
                perror("select");                
            }            
            else if(retavl > 0)
            {    //有文件就绪
                。。。。        
            }            
            else if(retval == 0)
            {    //等待超时
                。。。。
            }            
    【辅助函数】:
1】.        void FD_ZERO(fd_set *set);            
        功能:初始化指定的集合,全部清零
        例如:
            FD_ZERO(sockfd,&rfds);                                
2】.        void FD_SET(int fd, fd_set *set);    
        功能:把指定的fd设置到集合中去
        例如:
            FD_SET(sockfd,&rfds);                            
3】.        int  FD_ISSET(int fd, fd_set *set);    
        功能:判断指定的fd是否在这个集合中
        返回值:
                成功:返回非0
                失败:返回0
        例如:
            FD_ISSET(sockfd,&rfds);                
4】.        void FD_CLR(int fd, fd_set *set);    
        功能:把指定fd从集合中移除
        例如:
            FD_CLR(sockfd,&rfds);
                                        
                                            
    select使用【步骤】:                    
    1.设置文件描述符                
        fd_set rfds,tmpfds;//创建两个文件描述符集合
        struct timeval tv;//定义一个超时时间结构体                
        int retval;        //定义一个描述符作为轮询函数的返回值,
        //用于划分轮询结果以便分配任务    
    2.    初始化指定的文件集合,全部清零    
        FD_ZERO(&rfds);//把文件描述符集合清零,即初始化
    3.    把指定的文件的文件描述符设置到集合中去    
        FD_SET(i, &rfds);//把文件描述符i,放到集合rfds中,即把对应的位置1    
    4.初始化时间结构体    ,
        /* 超时时间设为5秒. */
        tv.tv_sec = 5;
        tv.tv_usec = 0;        
    5.备份需要轮询的文件描述符集合
        tmpfds = rfds;//拷贝准备好的描述符集合,
        //每循环一次,备份一次文件描述符,启动一次轮询函数    
        //防止源集合被覆盖                    
    6.启动轮询函数                    
        retval = select(fd_max+1,&tmpfds,NULL,NULL,&tv);    
    7.判断指定的fd是否在这个集合中
        FD_ISSET(i,&rfds)    //判断指定的文件是否在集合中                                                
    8.    把指定fd从集合中移除                                        
        FD_CLR(i,&rfds);//从集合中移除这个连接套接字描述符                        
    9.    关闭文件
        close(i);


【TCP与UDP协议的比较】:
    1.tcp协议:
        传输控制协议  类似打电话
        面向连接的    (建立连接-->进行通信-->断开连接)
        在通信的整个过程中必需保持连接
        该协议保证了数据的传递是可靠且有序的
        属于全双工的字节流通信方式
        服务器压力较大,资源消耗大,执行效率较低
        
    2.udp协议:
        用户数据报协议
        面向非连接的    类似发短信
        在通信的整个过程中不需要保持连接
        不保证数据的可靠性和有序性
        属于全双工的数据报通信方式
        服务器压力较小,资源消耗小,执行效率高
                                                                                                    
【3】.基于【UDP协议】的通信模型                                                            
【注意】:
        1.UDP不分服务器和客户端
        2.    UDP调用接收函数必须要绑定地址,
        要用socklen_t len = sizeof(saddr);
        求出目标地址长度,在函数中用&len代表长度        
        3.调用方式函数不用绑定地址,也不要先求出长度
        
用法:                                                                        
    1.创建监听套接字                                                                    
        使用socket()函数                                                                         
    2.准备通信地址
        定义并初始化结构体变量    
/******************接收方**************/
    3.绑定监听套接字和通信地址
        使用bind()函数                            
    4.通信                                    
————————————————————————————
                                                                            
接收数据报函数:recvfrom()                                                                                
原型:ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                     struct sockaddr *src_addr, socklen_t *addrlen);        
即:recvfrom(监听套接字描述符,强转的被发送的数据首地址,
                发送数据的大小,发送标志,
                发送数据的目标地址,目标地址的大小);
作用:
        将目标地址发过来的N个字节存到缓冲区中
例如:
        socklen_t len = sizeof(saddr);
        ret = recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&saddr,&len);
sockfd:        socket函数的返回值,监听套接字                                                                        
buf:                被发送的数据首地址                                                                    
sizeof(buf):    发送数据的大小                                                                        
0:                发送标志
    0:功能与write一样,即阻塞
    MSG_DONTWAIT:        非阻塞                                                                        
(struct sockaddr *)&saddr:发送数据的目标地址                                                                
&len:目标地址的大小

**********************************/    
/***************发送方*************/
3.通信                                    
————————————————————————————
发送数据报函数:sendto()
原型:ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                      const struct sockaddr *dest_addr, socklen_t addrlen);
即:sendto(监听套接字描述符,强转的被发送的数据首地址,
                发送数据的大小,发送标志,
                发送数据的目标地址,目标地址的大小);
作用:
        将准备好的数据中的N个发送给大小为M的目标地址,进行通信
例如:
        int res = sendto(sockfd,str,strlen(str),0,
        (struct sockaddr *)&saddr,sizeof(saddr));
sockfd:        socket函数的返回值,监听套接字                                                                        
str:                被发送的数据首地址                                                                    
strlen(str):    发送数据的大小                                                                        
0:                发送标志
    0:功能与write一样,即阻塞
    MSG_DONTWAIT:        非阻塞                                                                        
(struct sockaddr *)&saddr:发送数据的目标地址    【发给谁】                                                            
sizeof(saddr):目标地址的大小                                                                        
                                                                            
    
**********************************/
5.关闭套接字
    close(sockfd);

【UDP】应用
            广播        组播        
发送方:1.要有一个套接字
            2.要有一个目标地址(不是给自己用的,所以不用绑定)
            3.要用专门的函数sendto()发送数据报
接收方:1.要有一个套接字
            2.要有一个给自己用的通信地址(需要绑定)和
            一个存储发送者资料的地址
            3.
            方法一:
            使用setsockopt()函数将默认接收的缓冲区设计为一个小的缓冲区,
            缓冲区地址和大小任意定义,使用getsockopt()函数得到套接字选项值
            例如:
                setsockopt(sockfd,SOL_SOCKET,SO_BROADCAST,(void*)&j,sizeof(j));
                getsockopt(sockfd,SOL_SOCKET,SO_BROADCAST,(void*)&i,&size);
            方法二:
            定义一个多播组结构体变量,并初始化,使用setsockopt()函数将默认接收
            的缓冲区设计为一个小的缓冲区,缓冲区地址为定义结构体变量的首地址,
            大小为结构体变量所占字节数
            例如://加入多播组的方式
            struct ip_mreq join_addr;
            memset(&join_addr,0,sizeof(join_addr));
            join_addr.imr_multiaddr.s_addr = inet_addr(argv[1]);// 组播组的IP地址。
            join_addr.imr_interface.s_addr = htonl(INADDR_ANY); // 本地某一网络设备接口的IP地址。        
            setsockopt(sockfd,IPPROTO_IP,IP_ADD_MEMBERSHIP,(void*)&join_addr,sizeof(join_addr));

            4.绑定监听套接字和通信地址
            5.使用专门的函数recvfrom()接收数据报
            
            http://blog.csdn.net/chary8088/article/details/2486377
【套接字】:
【概念】:通信的两方的一种约定,用套接字中的相关函数来完成通信过程
            
【套接字选项】:
【概念】:每个【套接字】在不同的协议层次上【有】不同的【行为属性】,
            那么这个【行为属性】就称为套接字选项
【作用】:    设置地址复用
                允许发送广播消息
                将主机加入多播组
                设置发送与接收缓冲区的大小等                        
                                
    我们正常使用套接字编程时,一般只关注数据通信,而忽略套接字的不同特性
    但有时需要设置地址复用,允许发送广播消息,将主机加入多播组,设置发送与接收缓冲区的大小等
    这些都需要对套接字选项进行设置。    
        
【所用函数】:            
    原型:int getsockopt(int sockfd, int level, int optname,
                      void *optval, socklen_t *optlen);
    即:getsockopt(监听套接字,套接字选项所在的协议层次,
                            套接字选项的名称,缓冲区首地址,
                            缓冲区的长度);
    功能:    得到套接字选项值                    
    返回值:
            成功:返回0。
            失败:返回-1,errno被设置
    例如:

    原型:int setsockopt(int sockfd, int level, int optname,
                      const void *optval, socklen_t optlen);
    即:setsockopt(监听套接字,套接字选项所在的协议层次,
                            套接字选项的名称,缓冲区首地址,
                            缓冲区的长度);
    功能:如果认为套接口的默认发送以及接收缓冲区的尺寸太大时,
    作为程序设计者的我们可以【将默认发送的缓冲区设计为一个小的缓冲区】。

    
将socket加入一个组播组,因为socket要接收组播地址224.0.0.1的
数据,它就必须加入该组播组。结构体struct ip_mreq mreq是
该操作的参数,下面是其定义:
    struct ip_mreq
    {
        struct in_addr imr_multiaddr;   // 组播组的IP地址。
        struct in_addr imr_interface;   // 本地某一网络设备接口的IP地址。
    };
    一台主机上可能有多块网卡,接入多个不同的子网,
    imr_interface参数就是指定一个特定的设备接口,
    告诉协议栈只想在这个设备所在的子网中加入某个组播组。
    有了这两个参数,协议栈就能知道:在哪个网络设备接口上
    加入哪个组播组。为了简单起见,我们的程序中直接写明了
    IP地址:在172.16.48.2所在的设备接口上加入组播组224.0.1.1。
    这个操作是在网络层上的一个选项,所以级别是SOL_IP
    IP_ADD_MEMBERSHIP选项把用户传入的参数拷贝成了
    struct ip_mreqn结构体:


 

你可能感兴趣的:(网络编程)