Linux网络编程

目录

网络编程基础

Internet历史

TCP/IP协议基本概念

网络体系结构

TCP/IP体系结构

TCP/IP协议知识要点

TCP协议和UDP协议

网络编程预备知识

基于TCP协议的网络编程案例

基于UDP协议的服务器客户端编写

1.UDP服务器编程流程

2.UDP客户端编程流程

API接口

笔试面试要点补充

I/O模型和服务器模型

1.Linux中的IO模型

2.IO多路复用思路

3.select函数

并发服务器模型

1.多进程实现并发服务器模型

2.多线程实现并发服务器模型

3.select实现并发服务器模型

4.poll实现并发服务器模型

Linux高级网络编程

网络超时检测

套接字接收检测

广播

组播

UNIX域套接字

UNIX域流式套接字服务器和客户端编程


网络编程基础

Internet历史

1、1957年,前苏联卫星上天

2、1958年,美国建立 ARPA(国防部高级研究计划署)

3、1968年,ARPA提出 资源共享计算机网络,简称ARPAnet,"阿帕网"。实现互联不同的计算机。

4、早起ARPAnet使用的网络控制协议 NCP(network control procotol)。 NCP不能互联不同类型的操作系统 和 不能互联不同类型的计算机,也没有纠错功能。

TCP/IP协议基本概念

1.TCP协议:用来检测网络传输中的出错,并具备一定的纠错能力

2.IP协议:实现互联不同类型的操作系统和计算机。

网络体系结构

由于网络是非常复杂的,因此采用分而治之的方法设计。因此网络体系结构是指网络的层次结构和每层使用的协议的集合。

OSI体系结构:七层模型,理想化的模型,尚未实现

        应用层:主要是一些应用程序,获得要传输的数据。

                        IFIP,HTTP,SNMP:简单网络管理协议,FIP,SMIP.DNS

        表示层:对数据进行格式定义,加密或者转换。

        会话层:建立通信进程的逻辑名字和物理名字之间的联系。

        传输层:提供可靠数据传输,差错处理及恢复(TCP),流量控制(UDP)

        网络层:数据分组,路由选择(IP,ICMP,IGMP等协议)

        数据链路层:数据组成可发送和接收的帧

        物理层:传输物理信号等

注意:

两路交换机:数据链路层的交换(硬件)

三路交换机:网络层的交换(软件)

路由器:运用到网络层数据链路层和物理层

TCP/IP体系结构

Internet事实上的工业标准,四层模型

应用层:(用户空间)

        常用协议(http:超文本传输协议,ftp:文件传输协议,dns:域名解析协议,SMTP邮件传输协议)

以下三层都在内核空间

传输层:确定数据包交给主机上那个任务

        常用协议(TCP:传输控制协议,UDP:用户数据报协议)

网络层:实现端到端的传输

        常用协议(IP:互联网络,ICMP:控制管理协议ping命令使用此协议,IGMP:广播组播)

网络接口和物理层:屏蔽硬件差异,向上层提供统一的接口

        常用协议:ARP:地址解析协议  IP地址->MAC地址

                            RARP:逆地址解析协议   MAC地址->IP地址

TCP/IP协议知识要点

1.MTU:最大传输单元,1500字节

2.MSS:最大报文长度:1460字节(appheader+user data)

3.TCP header 和IP header都为 20字节

TCP协议和UDP协议

1.TCP协议:传输控制协议,面向连接,数据传输安全可靠(数据无失序,无重复,无丢失)

常用场景:

        1.对安全要求较高的场合(密码传输)QQ, 微信的登陆注册

        2.大量的数据传输

特点:

        是一种面向连接的传输层协议,建立连接管道,因此数据传输效率相对较低

2.UDP协议:传输控制协议,无连接,不保证数据的安全可靠性

常用场景:

        1.少量图片等数据传输。QQ,微信聊天

        2.音视频,流媒体等数据传输

        3.无线网路传输

        4.广播组播通信

特点:

        由于无连接,不用建立连接管道,因此数据传输效率相对高。

网络编程预备知识

1.socket

1.是一种通用的编程接口

2.也是一个文件描述符(套接字)

3.可用于TCP,UDP,IPX通信

2.socket的类型

1.流式套接字(SOCK_STREAM):提供一种面向连接的服务,用于TCP传输控制协议的。

2.数据报套接字(SOCK_DGRAM):提供无连接的服务,用于UDP通信。

3.原始套接字(SOCK_RAW):提供底层通信(IP,ICMP,IGMP)。

3.IP地址:网络中标识主机的编号,常以点分形式存在(192.16..2.17)

1.IPV4地址:整个IP占32位,每段8位表示0~255

2.IPV6地址:整个IP占128位

3.IP地址由:网络号+主机号-->"192.168.2"+"177"

                    其中:“2”表示网段号,也就是在哪个路由网络中(网络号+1)

4.网络ip分类:主要由ip的(第一段)也就是前八位数据组成

        A类网:0-127     子网掩码:255.0.0.0

        B类网:128-191   子网掩码:255.255.0.0

        C类网:192-223     子网掩码:255.255.255.0

        D类网:224-239  组播地址

        E类网:240-255  保留测试地址

5.私有ip:

6.子网掩码:例如 255.255.255.255

7.默认C类网已知ip   计算网络号:ip & (umask)

                                    计算主机号:ip & (~umask)

4.由于网络中只能识别2进制数据,因此ip地址必须进行转换

1.将代码字符串的ip地址转换成网络字节序的二进制,并返回转换后的地址

        in_ addr_t  inet_addr(const char *strptr);

        头文件

                #include <arpa/inet.h>

        参数:

                strptr:代码中的字符串ip(主机字节序的ip地址)

        返回值:

                成功:返回网络字节序二进制的首地址

2.将网络字节序的二进制转换成主机字节序的ip

        char *inet_ntoa(struct in_addr inaddr)

        头文件:

                #include <arpa/inet.h>

        参数:

                inaddr:网络字节序的ip地址

        返回值:

                成功:主机字节序的ip字符串首地址

5.字节序:数据的存储顺序(方式)

1.小端序:低序字节存储低地址(ubuntu采用小端序,也就是主机字节序)

2.大端序:低序字节存储高地址(网络采用大端序,也就是网络字节序)

例:0x1122->11是高序字节

6.端口号:在主机中标识处理网络数据的进程,也就是进程ID。

1.众所周知的端口号(已被使用,用户一般不能使用):1~1023

2.已登记的端口号:1024~49151

3.动态端口号:49152~65535

4.主机字节序转网络字节序

        u_long htonl(int port);//转为长整型 host to network long

        u_short htons(int port)//转为短整型 host to network short

5.网络字节序转主机字节序

        u_long ntohl(u_long hostlong)//network to host long

        u_short ntohs(u_short hostshort)//network to host short

7.客户端:与用户进行数据交互的界面程序,即上位机。

   服务器:为客户端提供数据服务的后台软件。

8.基于tcp的服务器编程流程

1.创建套接字 socket() 

2.绑定 bind()

3.监听 listen()

4.接受连接 accept()

5.数据交互 recv/send  read/write

6.关闭 close()

9.基于tcp的客户端编程流程

1.创建套接字 socket() 

2.绑定 bind()   (主要用于服务器端,客户端可以不绑定地址)

注:此处绑定是将客户端自身ip,端口号与套接字进行绑定,目的是人为指定客户端进程,否则将会由系统自动分配,将客户端与某个端口号进行关联绑定,目的是服务器接受连接之后,可以根据端口号判断是哪个客户端。

3.发起连接请求 connect()

4.数据交互 recv/send  read/write

5.关闭 close()

10.API接口

1.函数原型:

        int socket(int domain,int type,int procotol);

        功能:

                创建套接字打开网卡设备,设置套接字类型

        头文件:

                #include <sys/types.h>

                #include <sys/socket.h>

        参数:

                domain:ip地址协议族 AF_INTE(ipv4协议)

                type:套接字类型  SOCK_STREAM(流式套接字类型)

                protocol:通常为0

        返回值:

                成功:返回套接字(网卡的文件描述符)

                失败:返回-1,并设置错误信息

2.函数原型:

        int bind(int sockfd,struct sockaddr* addr,size_t size);

        功能:

                将ip地址,端口号与套接字进行绑定,将ip和端口共享到网络中。

        头文件:

                #include <sys/types.h>

                #include <sys/socket.h>

        参数:

                sockfd:套接字,socket函数成功的返回值。

                addr:通用协议地址结构体首地址

                size:结构体大小

        通用协议地址结构体:

                struct sockaddr{

                        sa_family_t sa_family;//地址协议族 AF_INET

                        char          sa_data[14];

                };

        Internet协议地址结构体:        

                        struct sockaddr_in {
                              sa_family_t      sin_family; /* 地址族: AF_INET */
                              in_port_t           sin_port; /* 按网络字节次序的端口 */
                              struct in_addr    sin_addr; /* internet地址 */
                              };

              /* Internet地址. */
                        struct in_addr {
                              uint32_t          s_addr; /* 按网络字节次序的地址 */
                              };

        结构体头文件:#include <arpe/inet.h>

        填写ip地址和端口号

                struct sockaddr_in addr;

                addr.sin_family  = AF_INET;//地址协议族

                addr.sin_port    =htons(9527);//端口号

                //INADDR_ANY(0.0.0.0)

                addr.sin_addr.s_addr   = inet_addr("0.0.0.0");//(运行时会绑定系统任意一个ip)服务器所在操作系统ip

        返回值:

                成功:0

                失败:-1

3.函数原型

        int listen(int sockfd,int backlog);

        功能:

                设置监听套接字,设置客户端连接请求队列的长度。

                也就是设置同一时刻能接受的最大请求数。(请求的信息放在队列中)

                监听完成就是启动服务器,套接字变成监听套接字。

        参数:

                sockfd:套接字

                backlog:请求队列的长度,在内核中开辟的。

        返回值:

                成功:返回0

                失败:返回-1,并设置错误信息

4.函数原型

        int accept(int sockfd,struct sockaddr* addr,socklen_t * len)

        功能:

                阻塞等待,接受客户端连接,建立通信管道。

                如果请求队列中没有客户端的请求,该函数就阻塞。有就立即接受连接,建立通道

        参数:

                sockfd:监听套接字

                addr:结构体首地址(存储连接成功的客户端IP,端口号的首地址)

                len:存储结构体大小首地址(存储客户端信息的结构体首地址)

        返回值:

                成功:返回连接套接字(通信管道的id号),标识成功的客户端。

                失败:返回-1,并设置错误信息

        注意:

                1.如果不需要保存客户端ip地址和端口号则2,3参数为NULL即可

                2.该函数调用一次就只能接受一个客户端连接。

5.函数原型

        功能:数据交互

        ssize_t read(int connfd,void* buf,size_t size)

        参数:

                connfd:连接套接字(通信管道id accept函数返回值)

        返回值:

                成功:实际读到的字节数

                出错:-1

                0:客户端关闭

        ssize_t recv(int connfd,void *buf,size_t len,int flags)

        参数:

                flags:阻塞与非阻塞模式,通常0表示阻塞

        注意:

                1.read读数据,默认为阻塞模式,如果读缓冲区有数据立即返回,无数据等待。

                2.recv读数据可以设置模式

                3.当连接断开时,read或recv立即返回0值。        

        sszie_t write(int connfd,void *buf,size_t  size)

        参数:

                connfd:连接套接字(通信管道id)

        ssize_t send(int connfd,void *buf,size_t len,int flags)

        参数:

                flags:阻塞与非阻塞模式,通常0表示阻塞

        

6.关闭套接字

int close(int sockfd);

7.函数原型

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

        功能:

                客户端通过该函数向服务器端的监听套接字发送连接请求。

        参数:

                sockfd:套接字描述符

                serv_addr:存放服务器端地址信息的结构体指针

                addrlen:存放服务器端地址信息的结构体大小

        返回值

                成功:0

                出错:-1

        注意:发起请求后,发送请求会存到服务端请求队列中,如果服务器不在线立刻返回-1,只要服务器在线立刻返回0值

                

基于TCP协议的网络编程案例

编写服务端和客户端代码,实现数据交互

服务端代码如下:

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
int main(int argc, char *argv[])
{ 
    //1.创建套接字
    int sockfd;
    sockfd=socket(AF_INET,SOCK_STREAM,0);
    if(sockfd<0)
    {
        perror("socket");
        return -1;
    }
    printf("%d\n",sockfd);
    //2.绑定
    struct sockaddr_in addr;//定义INTERNET协议地址结构体变量,用来存储IP和PORT 
    addr.sin_family=AF_INET;//INTERNET协议 IPV4 
    addr.sin_port=htons(9526);//端口号,要转换成网络字节序
    addr.sin_addr.s_addr=inet_addr("0.0.0.0");//0地址,绑定当前系统任意IP,要转换成网络字节序
    
   if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))<0)
   {
        perror("bind");
        return -1;
   }
   //3.监听
   if(listen(sockfd,5)<0)
   {
        perror("listen");
        return -1;
   }
   printf("server init success\n");
   //4.接受连接
   while(1)
   {     
    struct sockaddr_in caddr;//定义结构体变量,用来保存成功接受的客户端信息
    socklen_t len=sizeof(caddr);
    int connfd;
    connfd= accept(sockfd,(struct sockaddr*)&caddr,&len);//返回值为连接套接字,通信管道ID
    if(connfd<0)
    {
        perror("accpet");
        return -1;
    }
    printf("clien ip:%s port:%u\n",inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port));
//输出的时候转换成主机字节序
    //5.数据交互
    char buf[32];
    while(1)
    {    
        bzero(buf,sizeof(buf));//对数组进行清0
        int ret=0;
        ret=read(connfd,buf,sizeof(buf));//读取数据
        if(ret<0)
         {
             perror("read");
             return -1;
         }
        else if(ret==0)
        {
            printf("client is leaved\n");
            break;
        }
        else
        {
            printf("%d\n",ret);
            printf("revc:%s\n",buf);
            if(!strncmp(buf,"quit",4))//读取下一个端口
            {
                break;
            }
            else if(!strncmp(buf,"exit",4))//退出服务端
            {
                close(connfd);
                close(sockfd);
                return -1;
            }
        }
        
    }
 }
    close(sockfd);
    return 0;
} 

客户端程序如下:

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
int main(int argc, char *argv[])
{ 
    //1.创建套接字
    int sockfd;
    sockfd=socket(AF_INET,SOCK_STREAM,0);
    if(sockfd<0)
    {
        perror("socket");
        return -1;
    }
    printf("%d\n",sockfd);
   
   //2.发起连接请求  
    struct sockaddr_in saddr;//定义结构体变量,用来保存成功接受的服务端信息
    saddr.sin_family=AF_INET;
    saddr.sin_port=htons(9526);//服务器端口信息
    saddr.sin_addr.s_addr=inet_addr("0.0.0.0");//服务器IP地址信息
    int cet=connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//发起连接请求
    if(cet<0)
    {
        perror("connect");
        return -1;
    }
    //3.数据交互
    char buf[32];
    while(1)
    {    
        bzero(buf,sizeof(buf));
        int ret=0;
        fgets(buf,sizeof(buf),stdin);
        ret=write(sockfd,buf,sizeof(buf));//向服务端发送数据
        if(ret<0)
         {
             perror("write");
             return -1;
         }
        
    }
    close(sockfd);
    return 0;
} 

补充知识点:

在客户端与服务端的数据交互过程中,服务器意外结束时,如果客户端继续向服务器发送数据,第一次会成功。数据存入通信管道缓冲区,但第二次会失败,内核会发送一个管道破裂的信号,终止程序运行。(读端关闭,写端可以写一次第二次会失败,写端关闭,读端返回0)

基于UDP协议的服务器客户端编写

1.UDP服务器编程流程

1.创建套接字 socket

2.绑定 bind

while(1)

{

        3.接受数据 recvfrom

        4.发送数据 sendto

}

5.关闭 close

2.UDP客户端编程流程

1.创建套接字 socket

2.绑定(可选)bind

while(1)

{

        3.发送数据 sendto

        4.接受数据 recvfrom

}

5.关闭 close

API接口

1.函数原型

        ssize_t recvfrom(int sockfd,void* buf,size_t len,int flag,struct sockaddr* src_addr,socklen_t * addrlen);

 功能:

        阻塞等待,接受数据,保存数据发送者的ip,端口信息。

头文件:

        #include <sys/types.h>

        #include <sys/socket.h>

参数:

        sockfd:套接字

        buf:存储数据的缓冲区首地址

        len:要读取的数据字节数

        flags:0(阻塞模式)

        src_addr:保存数据发送端的ip和端口号的结构体首地址

        addrlen:存储结构体的长度

返回值:

        成功:实际读取的字节数

        失败:-1,并设置错误信息

2.函数原型

        ssize_t sendto(int sockfd,const void* buf,size_t len,int flag,const struct sockaddr* dest_addr,socklen_t   addrlen);

 功能:

        将buf地址上的前len个字节数据,写入到sockfd网络中,发送到dest_addr对应的目标主机中

头文件:

        #include <sys/types.h>

        #include <sys/socket.h>

参数:

        sockfd:套接字

        buf:要发送的数据的缓冲区首地址

        len:要发送的数据字节数

        flags:0(阻塞模式)

        dest_addr:存储目标主机的ip和端口号的结构体首地址

        addrlen:结构体的长度

返回值:

        成功:实际发送的字节数

        失败:-1,并设置错误信息

我们先来看看服务器端的代码吧:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <strings.h>
int main(int argc, char *argv[])
{ 
    if(argc<3)
    {
        printf("Usage:%s port ip\n",argv[0]);
        return -1;
    }
    //1.创建套接字
    int sockfd;
    sockfd=socket(AF_INET,SOCK_DGRAM,0);
    if(sockfd<0)
    {
        perror("socket");
        return -1;
    }
    //2.绑定,共享IP端口号
    struct sockaddr_in addr;
    addr.sin_family=AF_INET;
    addr.sin_port=htons(atoi(argv[1]));
    addr.sin_addr.s_addr=inet_addr(argv[2]);//htonl(INADDR_ANY)
    if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))<0)
    {
        perror("bind");
        return -1;
    }
    char buf[32]={0};
    struct sockaddr_in dest_addr;
    socklen_t len=sizeof(buf);
    while(1)
    {
        bzero(buf,sizeof(buf));
        bzero(&dest_addr,sizeof(dest_addr));
        //接收目标客户端的数据
        int ret=recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr*)&dest_addr,&len);
        if(ret<0)
        {
            perror("recvfrom");
            continue;
        }
        printf("recv:%s from: %s   %u\n",buf,inet_ntoa(dest_addr.sin_addr),ntohs(dest_addr.sin_port));
        //给目标客户端发送数据
        bzero(buf,sizeof(buf));
        fgets(buf,sizeof(buf),stdin);
        ret=sendto(sockfd,buf,strlen(buf),0,(struct sockaddr*)&dest_addr,sizeof(dest_addr));
        if(ret<0)
        {
            perror("sendto");
            continue;
        }
        if(strncmp(buf,"exit",4)==0)
        {
            break;
        }
        
    }
    close(sockfd);
    return 0;
} 

客户端代码如下:

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
int main(int argc, char *argv[])
{ 
    if(argc<3)
    {
        printf("Usage:%s port ip\n",argv[0]);
        return -1;
    }
    //1.创建套接字
    int sockfd;
    sockfd=socket(AF_INET,SOCK_DGRAM,0);
    if(sockfd<0)
    {
        perror("socket");
        return -1;
    }
    //存放服务端的IP和端口信息
    struct sockaddr_in saddr;
    saddr.sin_family=AF_INET;
    saddr.sin_port=htons(atoi(argv[1]));
    saddr.sin_addr.s_addr=inet_addr(argv[2]);
    char buf[32]={0};
    while(1)
    {
        bzero(buf,sizeof(buf));
        fgets(buf,sizeof(buf),stdin);
        //向服务器发送数据
        int ret=sendto(sockfd,buf,strlen(buf)-1,0,(struct sockaddr*)&saddr,sizeof(saddr));
        if(ret<0)
        {
            perror("sendto");
            continue;
        }
        printf("send num:%d\n",ret);
        bzero(buf,sizeof(buf));
        ret=recvfrom(sockfd,buf,sizeof(buf),0,NULL,NULL);//接受服务器的数据,由于这里不需要保存服务器的信息所有后两个参数为NULL
        if(ret<0)
        {
            perror("recvfrom");
            continue;
        }
        printf("recv from:%s\n",buf);
        if(strncmp(buf,"exit",4)==0)
        {
            break;
        }
    }
    close(sockfd);
    return 0;
} 

笔试面试要点补充

1.网络粘包(网络风暴)

数据的读写数据率不匹配,就会导致多余数据包粘连在读缓冲区中,解决方法有下面两种。

1.调整读写速率

2.调整读取的每条数据包大小。

2.客户端连接服务器成功的过程(三次握手建立TCP连接)

注意这个连接(发生在connect函数阶段)必须是一方主动打开,另一方被动打开的

第一次:

首先客户端向服务器端发送一段TCP报文,其中:

        标记位为SYN,表示“请求建立新连接”;

        序号为Seq=X(X一般为1);

        随后客户端进入SYN-SENT阶段。

第二次:

服务器端接收到来自客户端的TCP报文之后,结束LISTEN阶段。并返回一段TCP报文,其中:

        标志位为SYN和ACK,表示“确认客户端的报文Seq序号有效,服务器能正常接收客户端发送的数据,并同意创建新连接”(即告诉客户端,服务器收到了你的数据);

        序号为Seq=y;确认号为Ack=x+1,表示收到客户端的序号Seq并将其值加1作为自己确认号Ack的值;

        随后服务器端进入SYN-RCVD阶段。

第三次:

客户端接收到来自服务器端的确认收到数据的TCP报文之后,明确了从客户端到服务器的数据传输是正常的,结束SYN-SENT阶段。并返回最后一段TCP报文。其中:

        标志位为ACK,表示“确认收到服务器端同意连接的信号”(即告诉服务器,我知道你收到我发的数据了);

        序号为Seq=x+1,表示收到服务器端的确认号Ack,并将其值作为自己的序号值;

        确认号为Ack=y+1,表示收到服务器端序号Seq,并将其值加1作为自己的确认号Ack的值;

        随后客户端进入ESTABLISHED阶段。服务器收到来自客户端的“确认收到服务器数据”的TCP报文之后,明确了从服务器到客户端的数据传输是正常的。结束SYN-SENT阶段,进入ESTABLISHED阶段。

思考一下那为什么要握手三次而不是两次呢?

“第三次握手”是客户端向服务器端发送数据,这个数据就是要告诉服务器,客户端有没有收到服务器“第二次握手”时传过去的数据。若发送的这个数据是“收到了”的信息,接收后服务器就正常建立TCP连接,否则建立TCP连接失败,服务器关闭连接端口。由此减少服务器开销和接收到失效请求发生的错误。

 3.客户端与服务器的断开过程(四次挥手)

所谓的四次挥手即TCP连接的释放(解除),发生在close函数阶段。连接的释放必须是一方主动释放,另一方被动释放

1.首先客户端想要释放连接,向服务器端发送一段TCP报文,其中:

        标记位为FIN,表示“请求释放连接“;

        序号为Seq=U;随后客户端进入FIN-WAIT-1阶段,即半关闭阶段。并且停止在客户端到服务器端方向上发送数据,但是客户端仍然能接收从服务器端传输过来的数据。

注意:这里不发送的是正常连接时传输的数据(非确认报文),而不是一切数据,所以客户端仍然能发送ACK确认报文。

2.服务器端接收到从客户端发出的TCP报文之后,确认了客户端想要释放连接,随后服务器端结束ESTABLISHED阶段,进入CLOSE-WAIT阶段(半关闭状态)并返回一段TCP报文,其中:

        标记位为ACK,表示“接收到客户端发送的释放连接的请求”;

        序号为Seq=V;确认号为Ack=U+1,表示是在收到客户端报文的基础上,将其序号Seq值加1作为本段报文确认号Ack的值;

        随后服务器端开始准备释放服务器端到客户端方向上的连接。客户端收到从服务器端发出的TCP报文之后,确认了服务器收到了客户端发出的释放连接请求,随后客户端结束FIN-WAIT-1阶段,进入FIN-WAIT-2阶段

        这两次挥手让服务器端知道了客户端想要释放连接,也让客户端知道了服务器端了解了自己想要释放连接的请求。于是,可以确认关闭客户端到服务器端方向上的连接了。

3.服务器端自从发出ACK确认报文之后,经过CLOSED-WAIT阶段,做好了释放服务器端到客户端方向上的连接准备,再次向客户端发出一段TCP报文

        标记位为FIN,ACK,表示“已经准备好释放连接了”。

        注意:这里的ACK并不是确认收到服务器端报文的确认报文。

        序号为Seq=W;确认号为Ack=U+1;表示是在收到客户端报文的基础上,将其序号Seq值加1作为本段报文确认号Ack的值。

        随后服务器端结束CLOSE-WAIT阶段,进入LAST-ACK阶段。并且停止在服务器端到客户端的方向上发送数据,但是服务器端仍然能够接收从客户端传输过来的数据。

4.客户端收到从服务器端发出的TCP报文,确认了服务器端已做好释放连接的准备,结束FIN-WAIT-2阶段,进入TIME-WAIT阶段,并向服务器端发送一段报文。

        标记位为ACK,表示“接收到服务器准备好释放连接的信号”。

        序号为Seq=U+1;表示是在收到了服务器端报文的基础上,将其确认号Ack值作为本段报文序号的值。

        确认号为Ack=W+1;表示是在收到了服务器端报文的基础上,将其序号Seq值作为本段报文确认号的值。

后“两次挥手”既让客户端知道了服务器端准备好释放连接了,也让服务器端知道了客户端了解了自己准备好释放连接了。于是,可以确认关闭服务器端到客户端方向上的连接了,由此完成“四次挥手”。

与“三次挥手”一样,在客户端与服务器端传输的TCP报文中,双方的确认号Ack和序号Seq的值,都是在彼此Ack和Seq值的基础上进行计算的,这样做保证了TCP报文传输的连贯性,一旦出现某一方发出的TCP报文丢失,便无法继续"挥手",以此确保了"四次挥手"的顺利完成。

I/O模型和服务器模型

1.Linux中的IO模型

1.阻塞IO:最简单,最常用,但效率最低(read/write)

2.非阻塞IO:防止进程阻塞在IO操作上,但必须使用轮询(浪费CPU资源)

3.IO多路复用:同时对多个IO进行操作,效率比阻塞高,浪费的CPU资源比非阻塞少。

4.信号驱动IO:异步通信模型

2.IO多路复用思路

1.先构造一张描述符集合的表

2.使用一个函数,检测该表中哪一个或者多个文件描述符可进行IO操作。该函数就立即返回该文件描述符的标志。

3.根据该函数返回的哪一个文件描述符,就可以进行IO操作。

select 函数       poll函数       epoll函数

3.select函数

函数原型:

        int select(int n,fd_set* read_fds,fd_set* write_fds,fd_set* except_fds,struct timeval* timeout)

        功能:函数允许程序监视多个文件描述符,等待所监视的一个或者多个文件描述符变为“准备好”的状态。所谓的”准备好“状态是指:文件描述符不再是阻塞状态,可以用于某类IO操作了,包括可读,可写,发生异常三种。

        参数:

        n: 表示集合中所有文件描述符的范围,即所有文件描述符的最大值+1

        注意, 待测试的描述集总是从0, 1, 2, …开始检测的。 所以, 假如你要检测的描述符为8, 9, 10, 那么系统实际也要监测0, 1, 2, 3, 4, 5, 6, 7, 此时真正待测试的描述符的个数为11个, 也就是max(8, 9, 10) + 1

        fd_set:一个文件描述符集合保存在fd_set变量中,可读,可写,异常这三个描述符集合需要使用三个变量来保存,分别是 read_fds,write_fds,except_fds。我们可以认为一个fd_set变量是由很多个二进制构成的数组,每一位表示一个文件描述符是否需要监视。

        对于fd_set类型的变量,我们只能使用相关的函数来操作。

        void FD_CLR(int fd, fd_set *set);//清除某一个被监视的文件描述符。
        int  FD_ISSET(int fd, fd_set *set);//测试一个文件描述符是否是集合中的一员
        void FD_SET(int fd, fd_set *set);//添加一个文件描述符,将set中的某一位设置成1;
        void FD_ZERO(fd_set *set);//清空集合中的文件描述符,将每一位都设置为0;

        readfds:

        监视文件描述符的一个集合,我们监视其中的文件描述符是不是可读,或者更准确的说,读取是不是不阻塞了。
        writefds:
        监视文件描述符的一个集合,我们监视其中的文件描述符是不是可写,或者更准确的说,写入是不是不阻塞了。
        exceptfds:
        用来监视发生错误异常文件

        timeout:超时检测

        struct timeval{

                 long tv_sec;//秒

                 long tv_usec;//微秒

         };

返回值:

        成功时:返回三中描述符集合中”准备好了“的文件描述符数量。
        超时:返回0
        错误:返回-1,并设置 errno

特别注意:由于select()修改其文件描述符集,如果调用在循环中使用,则必须在每次调用之前重新初始化这些集!!!!(select函数一次只能检测一个文件描述符,未检测到变化会一直阻塞,但我们可以设置超时检测,超时返回0)

我们来看一代代码理解一下select函数的用法:

#include <sys/select.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
	int fd = open("/dev/input/mouse0", O_RDONLY);
	if (fd < 0){
		perror("open");
		return -1;
	}
	

	//创建 文件描述符集合
	fd_set rfd1, rfd2;
	
	//清空集合
	FD_ZERO(&rfd1);
	//将带操作的文件描述符加入 集合中
	FD_SET(0, &rfd1);          //加入键盘的文件描述符
	FD_SET(fd, &rfd1);		   //加入鼠标的文件描述符
	
	int maxfd = fd + 1;
	while(1){
		// 由于select检测时,如果集合中有可读文件描述符,则该集合会被设置标志,
		// 使用完成不会自动清除标志,如果不做任何处理,则下次循环,就不会设置新的标志
		rfd2 = rfd1;   //使用新的结集合覆盖 集合中的所有数据清除标志位。
		//select检测集合中哪个文件描述符可操作,原理:函数内部自动遍历 0~maxfd之间的所有文件描述符,加入集合的文件描述符才能返回标志
		// 第2参数:描述符可读的集合     
        //参数3:描述符可写集合,NULL表示不检测;  参数4:NULL不检测; 参数5:NULL一直阻塞
		// select函数在参数 5 规定时间内检测集合中,是否有可操作文件描述符。
		// 如果没有,则阻塞;如果有则返回可操作描述符的数量,并设置标志
		// 如果时间到了,则返回 0
		int ret = select(maxfd, &rfd2, NULL, NULL, NULL);
		if(ret < 0){
			perror("select");
			continue;
		}
		printf("ret: %d\n", ret);
		//检测判断 集合中被置位的文件描述符是谁(检测判断哪个文件描述符可操作)
		if ( FD_ISSET(0, &rfd2) ){
			char buf[32] = {0};
			read(0, buf, sizeof(buf));
			printf("keyBoard: %s\n", buf);
		} else if( FD_ISSET(fd, &rfd2) ){
			char buf[32] = {0};
			read(fd, buf, sizeof(buf));
			printf("mouse: %s\n", buf);
		}
	}
	return 0;

}

2.函数原型:

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

功能:

        监视并等待多个文件描述符的属性变化

参数:

        fds:指向一个结构体数组的第0个元素的指针,每个数组元素都是一个struct pollfd结构,用于指定测试某个给定的fd的条件

        struct pollfd{
            int fd;            //文件描述符
            short events;    //等待的事件
            short revents;    //实际发生的事件events 域中请求的任何事件都可能在 revents 域中返回.
        };

        nfds:用来指定第一个参数数组元素个数

        timeout:指定等待的毫秒数,无论 I/O 是否准备好,poll() 都会返回

返回值

        成功时,poll() 返回结构体中 revents 域不为 0 的文件描述符个数;

        如果在超时前没有任何事件发生,poll()返回 0;

        失败时,poll() 返回 -1

注意:poll函数中revents每次置位后不会自动清除,要人为清0不然一直执行上一次的操作!!

我们来看一段代码理解一下poll的用法

#include <sys/select.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <poll.h>
int main(int argc, char *argv[])
{
	int fd = open("/dev/input/mouse0", O_RDONLY);
	if (fd < 0){
		perror("open");
		return -1;
	}
	//由于要检测的 文件描述符只有 2个,因此数组元素 2个
	struct pollfd pfd[2];
	

	//向数组元素中装填 要检测的文件描述符和 事件
	pfd[0].fd = 0;				//文件描述符
	pfd[0].events = POLLIN;      //带检测的事件:数据可读
	
	pfd[1].fd = fd;
	pfd[1].events = POLLIN;
	
	while(1){
		
		// 参数1:要检测的文件描述符和事件的结构体数组 首地址
		// 参数2:要检测的文件描述符个数
		// 参数3:超时设置, 单位 毫秒
		// 成功:返回 非负值(可操作文件描述的元素的个数);超时:返回 0; 失败:返回-1
		int ret = poll(pfd, 2, 5000);
		if(ret < 0){
			perror("poll");
			continue;
		} else if (ret == 0){
			printf("time out\n");
			continue;
		}
		for(int i = 0; i < 2; i++){
			if( pfd[i].revents & pfd[i].events ){ //判断数组中哪个元素的 revents 被置位
				if( pfd[i].fd == 0){			  //判断数组中产生事件的元素中 文件描述符为 谁
					char buf[32] = {0};
					read(pfd[i].fd, buf, 32);
					printf("keyBoard: %s\n", buf);
				} else if ( pfd[i].fd == fd){
					char buf[32] = {0};
					read(pfd[i].fd, buf, 32);
					printf("mouse: %s\n", buf);
				}
			}
		}
	}
	return 0;

}

并发服务器模型

1.多进程实现并发服务器模型

子进程只会拷贝fork函数之前的内容注意!!

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
#include <sys/wait.h>
#include <signal.h>
int server_init(int port)
{
    //1.创建套接字
    int sockfd;
    sockfd=socket(AF_INET,SOCK_STREAM,0);
    if(sockfd<0)
    {
        perror("socket");
        return -1;
    }
    //2.绑定
    struct sockaddr_in addr;//定义INTERNET协议地址结构体变量,用来存储IP和PORT 
    addr.sin_family=AF_INET;//INTERNET协议 IPV4 
    addr.sin_port=htons(port);//端口号
    addr.sin_addr.s_addr=inet_addr("0.0.0.0");//0地址,绑定当前系统任意IP

    if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))<0)
    {
        perror("bind");
        return -1;
    }
    //3.监听
    if(listen(sockfd,5)<0)
    {
        perror("listen");
        return -1;
    }
    return sockfd;
}
int waitForClient(int listenfd)
{

    struct sockaddr_in caddr;//定义结构体变量,用来保存成功接受的客户端信息
    socklen_t len=sizeof(caddr);
    int connfd;
    connfd= accept(listenfd,(struct sockaddr*)&caddr,&len);//返回值为连接套接字,通信管道ID
    if(connfd<0)
    {
        perror("accept");
        return -1;
    }
    printf("client ip:%s  port:%u\n",inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port));
    return connfd;
}
void processData(int connfd)
{
    while(1)
    {
        char buf[32]={0};
        int ret=read(connfd,buf,32);
        if(ret<0)
        {
            perror("read");
            break;
        }
        else if(ret==0)
        {
            printf("Client leave\n");
            break;
        }
        printf("recv:%s\n",buf);
        if(strncmp(buf,"1:",2)==0)
        {
            write(connfd,"hello",5);
        }
        else if(strncmp(buf,"2:",2)==0)
        {
            write(connfd,"welcome",7);
        }
        else if(strncmp(buf,"3:",2)==0)
        {
            write(connfd,"bye",3);
        }
    }
    close(connfd);
}
void handler(int no)
{
    waitpid(-1,NULL,WNOHANG);
}
int main(int argc, char *argv[])
{

    int sockfd=server_init(9527);
    if(sockfd<0)
    {
        return -1;
    }
    //注册信号和处理函数
    //当子进程结束时,内核触发SIGCHILD信号,就应该回收资源
    signal(SIGCHLD,handler);
    while(1)
    {
        int connfd=waitForClient(sockfd);
        if(connfd<0)
        {
            continue;
        }
        //为连接成功的客户端创建子进程,子进程只会拷贝fork之前的内容
        pid_t pid=fork();
        if(pid<0)
        {
            perror("fork");
            continue;
        }
        else if(0==pid)
        {
            //与客户端进行数据交互
            processData(connfd);
            exit(0);//结束子进程
            
        }
        

    }
    close(sockfd);
    return 0;
} 

2.多线程实现并发服务器模型

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
#include <sys/wait.h>
#include <pthread.h>
char res[32]={0};
void *thread(void* arg)
{
    int connfd=*(int*)arg;
    while(1)
    {
        char buf[32]={0};
        int ret=read(connfd,buf,32);
        if(ret<0)
        {
            perror("read");
            break;
        }
        else if(ret==0)
        {
            printf("client leave\n");
            break;
        }
        printf("recv:%s\n",buf);
        if(strncmp(buf,"1:",2)==0)
        {
            strcpy(res,"hello bro");
        }
        else if(strncmp(buf,"2:",2)==0)
        {
            printf("res:%s\n",res);
            write(connfd,res,strlen(res));
        }
        else if(strncmp(buf,"3:",2)==0)
        {
            write(connfd,"good",4);
        }
    }
    close(connfd);
    pthread_exit(NULL);

}

int main(int argc, char *argv[])
{

    int sockfd=server_init(9527);
    if(sockfd<0)
    {
        return -1;
    }
    while(1)
    {
        int connfd=waitForClient(sockfd);
        if(connfd<0)
        {
            continue;
        }
        pthread_t tid;
        if(pthread_create(&tid,NULL,thread,&connfd)<0)
        {
            printf("thread create error\n");
            continue;
        }
        pthread_detach(tid);

    }
    close(sockfd);
    return 0;
} 

3.select实现并发服务器模型

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
int main(int argc, char *argv[])
{

    int sockfd=server_init(9527);
    if(sockfd<0)
    {
        return -1;
    }
    //创建描述符集
    fd_set fd1,fd2;
    FD_ZERO(&fd1);
    FD_SET(sockfd,&fd1);
    int maxfd=sockfd;
    while(1)
    {     
        fd2=fd1;
        struct timeval val={5.0};
        if(select(maxfd+1,&fd2,NULL,NULL,&val)<=0)
        {
            printf("select error or time out\n");
            continue;
        }
        for(int i=0;i<maxfd+1;i++)//将集合中0~maxfd中每个文件描述符都进行遍历判断
        {
             
            if(FD_ISSET(i,&fd2))
             {
                 if(i==sockfd)//判断当前 集合中产生变化的 i是否是 sockfd
                 {     
                     int connfd= waitForClient(sockfd);
                     if(connfd<0)
                         continue;
                     }
                     FD_SET(connfd,&fd1);//将客户端的连接套接字加入集合,下次循环进行检测
                     maxfd=maxfd>connfd?maxfd:connfd;
                 }
                 else//只要集合中发生变化的不是 sockfd,那就一定是 connfd(连接套接字)
                 {
                    char buf[32]={0};
                    int ret=read(i,buf,32);
                    if(ret<0)
                    {
                        perror("read:");
                        continue;
                    }
                    else if(ret==0)
                    {
                        printf("client leaved\n");
                        FD_CLR(i,&fd1);
                        for(int j=0;j<=maxfd;j++)
                        {
                            if(j>=maxfd)
                                maxfd=j;
                        }
                        close(i);
                        continue;

                    }
                    else
                    {
                        printf("recv:%s\n",buf);
                    }
                 }
             }
        }
    }
    close(sockfd);
    return 0;
} 

4.poll实现并发服务器模型

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
#include <poll.h>

int main(int argc, char *argv[])
{

    int sockfd=server_init(9527);
    if(sockfd<0)
    {
        return -1;
    }
    char buf[32]={0};
    //定义存放文件描述符和检测事件的结构体数组
    struct pollfd pfd[1024];
    for(int i=0;i<1024;i++)
    {
        pfd[i].fd=-1;
        pfd[i].events=-1;
    }
    pfd[0].fd=sockfd;//要监听的第一个文件描述符
    pfd[0].events=POLLIN;//监听普通读事件
    int count=1;
    while(1)
    {
        int ret=poll(pfd,count,2000);
        if(ret<=0)
        {
            printf("poll error or time out\n");
            continue;
        }
        
        for(int i=0;i<count;i++)
        {
            if(pfd[i].revents & POLLIN)
            {
                if(pfd[i].fd==sockfd)
                {
                    int connfd=waitForClient(sockfd);
                    if(connfd<0)
                    {
                        break;
                    }
                    pfd[count].fd=connfd;
                    pfd[count].events=POLLIN;
                    count++;
                }
                else
                {
                    char buf[32]={0};
                    int ret=read(pfd[i].fd,buf,32);
                    if(ret<0)
                    {
                        
                    }
                    else if(ret==0)
                    {
                        close(pfd[i].fd);
                        pfd[i].fd=-1;
                        pfd[i].events=-1;
                    }
                    else
                    {
                        printf("recv:%s\n",buf);
                    }
                }
            }
        }
    }
    close(sockfd);
    return 0;
} 

Linux高级网络编程

网络超时检测

在网络通信过程中,经常出现一些突然情况,例如网络线路故障,或者通信一方异常结束,一旦出现这些情况,可能长时间都无法接收到数据,且无法判断没有数据还是数据无法到达,TCP协议中我们可以检测出来,但是在UDP中,我们需要在程序中进行超时检测。

套接字接收检测

套接字的选项很多,分属不同的协议层并有其对应的数据类型,通过设置套接字选项来进行超时检测。

1.函数原型

        int setsockopt(int sockfd,int level,int optname,const void *optval,socklen_t optlen)

   参数:

        sockfd:套接字描述符

        level:选项所属协议层 (SOL_SOCKET ,IPPROTO_IP ,IPPROTO_TCP)

        optval:设置选项值的缓冲区

        optlen:选项值的长度

   返回值:

        成功:0

        出错:-1

演示一段代码:

 //设置数据接收超时,如果是在TCP中客户端超时未发数据,该进程会自动关闭与客户端的连接
    struct timeval v={2};
    if(setsockopt(connfd,SOL_SOCKET,SO_RCVTIMEO,&v,sizeof(v))<0)
    {
            perror("setsockopt");
            return (void*)-1;
    }

在这个多线程并发服务器程序中我们将套接字超时检测放在接受客户端请求之后,如果客户端超时未发送数据,该进程会自动关闭与客户端的连接

广播

1.广播地址

        每个网段都有其对应的广播地址,每个网段最大的ip地址就是该网段的广播地址(例如:192.168.1.0这个网段的广播地址为192.168.1.255),当我们向这个地址发送数据包时,该网段的所有主机都会接收并处理。所以在发送广播包的时候我们的目标IP为广播地址。

2.广播包的发送流程

1.创建UDP套接字

2.指定目标地址(广播地址)和端口号

3.设置套接字选项允许发送广播包

4.发送数据包

来看一段代码具体分析

#include <stdio.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <string.h>
int main(int argc, char *argv[])
{ 
    //1.创建套接字
    int sockfd=socket(AF_INET,SOCK_DGRAM,0);
    if(sockfd<0)
    {
        perror("socket");
        return -1;
    }
    //设置运行套接字发送广播
    int on=1;
    if(setsockopt(sockfd,SOL_SOCKET,SO_BROADCAST,&on,sizeof(on))<0)
    {
        perror("setsockopt");
        return -1;
    }
    //指定接收方地址和端口号
    struct sockaddr_in addr;
    addr.sin_family=AF_INET;
    addr.sin_port=htons(8888);//端口号
    addr.sin_addr.s_addr=inet_addr("192.168.2.255");//目标广播地址
    //3发送数据
    while(1)
    {
        char buf[128]={0};
        fgets(buf,128,stdin);
        int ret=sendto(sockfd,buf,strlen(buf)-1,0,(struct sockaddr*)&addr,sizeof(addr));
        if(ret<0)
        {
            printf("error\n");
            continue;
        }
        printf("send:%s\n",buf);
    }
    return 0;
} 

3.广播包的接收流程

1.创建UDP套接字

2.绑定地址和端口

3.接收数据包

直接看代码:

#include <stdio.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
int main(int argc, char *argv[])
{ 
    //1.创建套接字
    int sockfd=socket(AF_INET,SOCK_DGRAM,0);
    if(sockfd<0)
    {
        perror("socket");
        return -1;
    }
    struct sockaddr_in saddr;
    saddr.sin_family=AF_INET;
    saddr.sin_port=htons(8888);
    //每个网段的最大IP就是广播地址
    saddr.sin_addr.s_addr=inet_addr("192.168.2.255");
    //2绑定广播地址和端口号
    if(bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr))<0)
    {
        perror("bind");
        return -1;
    }
    //3接收数据
    while(1)
    {
        struct sockaddr_in caddr;//保存发送端的信息
        socklen_t len=sizeof(caddr);
        char buf[128]={0};
        int ret=recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr*)&caddr,&len);
        if(ret<0)
        {
            printf("error\n");
            continue;
        }
        printf("recv from:%s :%s\n",inet_ntoa(caddr.sin_addr),buf);
    }
    return 0;
} 

组播

通过广播可以很方便实现发送数据包给局域网中的所有主机但频繁的发送广播包会造成所有主机数据链路层都来接收并交给上层协议处理,容易造成局域网的网络风暴,为此我们采用组播的方式来给加入指定多播组的主机发送数据包。

1.组播地址

D类地址我们又称为组播地址,每一个组播地址都代表一个多播组(224.0.0.1-239.255.255.255)

2.组播包的发送流程

1.创建UDP套接字

2.指定目标地址和端口(组播地址)

3.发送数据包

来看看代码;

#include <stdio.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <string.h>
int main(int argc, char *argv[])
{ 
    //1.创建套接字
    int sockfd=socket(AF_INET,SOCK_DGRAM,0);
    if(sockfd<0)
    {
        perror("socket");
        return -1;
    }
    //设置运行套接字发送广播
    int on=1;
    if(setsockopt(sockfd,SOL_SOCKET,SO_BROADCAST,&on,sizeof(on))<0)
    {
        perror("setsockopt");
        return -1;
    }
    //指定接收方地址和端口号
    struct sockaddr_in addr;
    addr.sin_family=AF_INET;
    addr.sin_port=htons(9527);
    addr.sin_addr.s_addr=inet_addr("231.0.0.1");//组播地址
    //3接收数据
    while(1)
    {
        char buf[128]={0};
        fgets(buf,128,stdin);
        int ret=sendto(sockfd,buf,strlen(buf)-1,0,(struct sockaddr*)&addr,sizeof(addr));
        if(ret<0)
        {
            printf("error\n");
            continue;
        }
        printf("send:%s\n",buf);
    }
    return 0;
} 

3.组播包的接收流程

1.创建UDP套接字

2.加入多播组(注意点)

3.绑定地址和端口

4.接收数据包

直接看代码理解吧:

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
int main(int argc, char *argv[])
{ 
    //1.创建套接字
    int sockfd=socket(AF_INET,SOCK_DGRAM,0);
    if(sockfd<0)
    {
        perror("socket");
        return -1;
    }
    //2.创建结构体变量存储组播和本机IP
    struct ip_mreq mreq;
    //填写组播地址 224.0.0.1~239.255.255.255之间
    mreq.imr_multiaddr.s_addr=inet_addr("231.0.0.1");//组播地址
    mreq.imr_interface.s_addr=inet_addr("0.0.0.0");//任意本机ip
    
    //3.将套接字加入多播组
    if(setsockopt(sockfd,IPPROTO_IP,IP_ADD_MEMBERSHIP,&mreq,sizeof(mreq))<0)
    {
        perror("setsockopt");
        return -1;
    }
    //4.绑定本地IP端口号
    struct sockaddr_in addr;
    addr.sin_family=AF_INET;
    addr.sin_port=htons(9527);
    addr.sin_addr.s_addr=htonl(INADDR_ANY);
    if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))<0)
    {
        perror("bind");
        return -1;
    }
    //5.接收数据
    while(1)
    {
        struct sockaddr_in caddr;//保存发送端信息
        socklen_t len=sizeof(caddr);
        char buf[128]={0};
        int ret=recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&caddr,&len);
        if(ret<0)
        {
            perror("recvfrom");
            continue;
        }
        printf("recv from:%s  : %s\n",inet_ntoa(caddr.sin_addr),buf);
    }
    return 0;
} 

UNIX域套接字

主要用于本地通信,同一系统下非亲缘进程间通信,使用简单,效率高,常用于前后台通信,又分为流式套接字和用户数据报类型。

本地地址:当套接字用于本地通信时,可以用结构体struct sockaddr_un描述一个本地地址

struct sockaddr_un{

        unsigned short sun_family;//协议类型 

        char sun_path[108];//套接字文件路径

UNIX域流式套接字服务器和客户端编程

服务器编程流程如下:

1.创建UNIX域流式套接字

2.绑定本地地址(套接字文件)

3.设置监听模式

4.接收客户端连接请求

5.数据交互

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/un.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
int main(int argc, char *argv[])
{ 
    //1.创建UNIX域流式套接字
    int sockfd=socket(PF_UNIX,SOCK_STREAM,0);
    int connfd;
    if(sockfd<0)
    {
        perror("socket");
        return -1;
    }
    //2.绑定本地地址(套接字文件)
    struct sockaddr_un myaddr;
    myaddr.sun_family=PF_UNIX;
    strcpy(myaddr.sun_path,"/tmp/mysocket1");//不存在则创建,对于字符串必须使用字符串函数
    if(bind(sockfd,(struct sockaddr*)&myaddr,sizeof(myaddr))<0)
    {
        perror("bind");
        return -1;
    }
    //3.监听
    if(listen(sockfd,10)<0)
    {
        perror("listen");
        return -1;
    }
    printf("server init success\n");
    //4.等待接收客户端连接请求
    while(1)
    {
        if((connfd=accept(sockfd,NULL,NULL))<0)
        {
            perror("accept");
            continue;
        }
        while(1)
        {
            char buf[128]={0};
    //5.接收数据
            int ret= recv(connfd,buf,sizeof(buf),0);
             if(ret<0)
             {
                 perror("recv");
                 close(connfd);
                 close(sockfd);
                 remove("/tmp/mysocket1");//套接字文件不能被重复使用
                 return -1;
             }
             else if(ret==0)
             {
                 printf("client leave\n");
                 break;
             }
             printf("recv:%s\n",buf);
        }
    }
    //6关闭套接字
    close(connfd);
    return 0;
} 

客户端编码流程如下:

1.创建UNIX域流式套接字。

2.指定服务器端地址(套接字文件)

3.建立连接

4.数据交互

注意:如果客户端需要绑定本地地址,需要一个新的套接字文件!!

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <arpa/inet.h>
#include <string.h>
int main(int argc, char *argv[])
{ 
    int sockfd=socket(PF_UNIX,SOCK_STREAM,0);
    if(sockfd<0)
    {
        perror("sockfd");
        return -1;
    }
    struct sockaddr_un saddr;
    saddr.sun_family=PF_UNIX;
    strcpy(saddr.sun_path,"/tmp/mysocket1");
    if(connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr))<0)
    {
        perror("connect");
        return -1;
    }
    while(1)
    {
        char buf[128]={0};
        fgets(buf,sizeof(buf),stdin);
        int ret= send(sockfd,buf,strlen(buf)-1,0);
        if(ret<0)
        {
            perror("send");
            continue;
        }
    }

    close(sockfd);
    return 0;
} 

UNXI用户数据报类型的编程流程其实和流式相差不大。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值