> 文档中心 > 第三十三篇,网络编程TCP协议通讯过程实现和函数接口

第三十三篇,网络编程TCP协议通讯过程实现和函数接口

网络编程知识点概览
========================================
   1.核心知识点
       传输层的两个重要协议
            tcp协议:
                tcp有关的理论概念(面试的时候喜欢问)
                linux中tcp通信流程和接口函数
                tcp单播(点播)
                tcp双向通信
                tcp广播
            udp协议
                udp有关的理论概念
                linux中udp通信流程和接口函数
                udp单向和双向通信
       多路复用
       tcp和udp的对比,三次握手,四次握手

   2.引入网络编程
       温故知新                                                                                                                          
       进程间通信:管道(pipe mkfifo) 信号(默认响应动作,改变响应动作kill和signal/sigqueue和sigaction,忽略信号,屏蔽/阻塞信号sigprocmask) 信号量(semget  semctl  semop) 共享内存(shmget shmat shmdt shmctl) 消息队列(msgget msgsnd msgrecv msgctl)
                   以上通信方法都只能在同一台主机内部进行通信
       网络编程也是属于进程间通信的一种方式,网络编程既可以在同一台主机内部通信,也可以在不同的主机间通信

网络中基本概念
========================================
  1.网络模型
       网络模型:为了方便大家理解整个网络的通信过程,人为把网络通信的过程划分为不同的层次
       通信协议:计算机科学家制定的游戏规则
                 比如:古代打仗的通信协议,敌人来了--》放狼烟
                       计算机网络中常见的通信协议 --》HTTP协议
                                                     FTP协议     文件传输协议
                                                     telnet协议  远程登陆协议
       路由:寻找网络数据传输的最优路径(有专门的算法去寻找最优路径)

       两种常见的网络模型:
            第一种: OSI七层模型              
                   应用层     作用:开发特定的应用程序需要用到其中的部分协议
                              比如:HTTP协议
                                    FTP协议     文件传输协议
                                    telnet协议  远程登陆协议
                   会话层     作用:建立会话,对数据加密,解密
                   表示层     作用:数据的封装,解析    http协议(超文本传输协议)  ftp协议(文件传输协议)  telnet(远程登录)
                   传输层     作用:解决数据在网络中传输的问题  tcp协议和udp协议
                   网络层(ip层)  作用:解决路由(数据采取何种路径发送出去最优)问题   ip协议
                   数据链路层   作用:开发网卡的驱动,需要深入了解这个层次的协议
                   物理层         网络接口,真实的网卡
            第二种:TCP/IP模型
                   依据OSI七层模型演变过来的
                   应用层(把应用层,会话层,表示层三个合并)
                   传输层 
                   网际层
                   网络接口层(把数据链路层和物理层合并)
                   

  2.MAC地址
       你电脑物理网卡的地址,是全世界独一无二
    地址协议族
       ip地址有两种,一种IPV4地址协议族,另外一种是IPV6地址协议族
    IPV4
       32位的IP地址,比如:192.168.22.2(点分十进制IP,三个小数点隔成四个部分,每个部分一个字节,小数点不占用存储空间,仅仅只是写法标记)
    IPV6
       为了解决IPV4地址不够用,扩展位数,128位   

  3.端口号
       作用:为了区分同一台主机内部不同的网络进程
             端口号本质上是个无符号的短整型数字  0---65535之间
             程序员是可以自己指定端口号,但是不能使用1024以内的端口号,因为1024以内的端口号很多都被操作系统占用了
             误解:端口号是不是就是进程的ID号--》不是,两个不同的概念

tcp协议的通信过程和接口函数
========================================
  1.tcp协议的通信过程/流程
       客户端的流程
           创建套接字--》绑定自己的ip和端口号--》连接服务器--》收发信息--》关闭套接字
       服务器的流程
           创建套接字--》绑定自己的ip和端口号--》监听--》接受客户端的连接请求--》收发信息--》关闭套接字
        
  2.相关的接口函数
      (1)创建tcp套接字--》买手机
            #include
            int socket(int domain, int type, int protocol);
                 返回值: 成功 返回套接字的文件描述符
                         失败 -1
                   参数:domain --》地址协议族类型
                             ipv4地址协议族 --》AF_INET
                             ipv6地址协议族 --》AF_INET6
                         type --》套接字的类型
                             tcp套接字/数据流套接字/流式套接字 --》SOCK_STREAM
                             udp套接字/数据报套接字           --》SOCK_DGRAM
                         protocol --》扩展协议,一般设置0,不会理会

      (2)绑定ip和端口号
            #include
            int bind(int socket, const struct sockaddr *address, socklen_t address_len); //既可以传递ipv4也可以传递ipv6
            假设:int bind(int socket, const struct sockaddr_in *address, socklen_t address_len);  //局限性
            假设:int bind(int socket, const struct sockaddr_in6 *address, socklen_t address_len); //局限性
                 返回值:成功 返回0
                         失败 -1
                   参数:socket --》套接字文件描述符
                         address --》用来存放你需要绑定的ip和端口号(自己的ip和端口号)
                                 struct sockaddr      //通用地址结构体(兼容ipv4和ipv6)
                                 {
                                     unsigned short sa_family;  //地址协议类型  AF_INET或者AF_INET6
                                     char sa_data[14];  //存放要绑定的ip和端口号
                                 }
                                 struct sockaddr_in   //ipv4地址结构体(专门用来存放ipv4地址)
                                 {
                                     sin_family; //存放地址协议族 AF_INET
                                     struct in_addr sin_addr;  //结构体嵌套,存放你要绑定的ipv4地址
                                     sin_port;   //存放你要绑定的端口号
                                 }
                                 struct in_addr
                                 {
                                     in_addr_t s_addr;;  //最终用来存放你需要绑定的ip地址
                                 } 
                                 struct sockaddr_in6  //ipv6地址结构体(专门用来存放ipv6地址)
                                 {

                                 }
                         address_len --》地址结构体的大小,sizeof

        (3)ip地址和端口号的转换--》大小端
             字节序:反映数据在计算机内存中采用何种存储方式去存储数据,分为两种,大小端
             不同的操作系统采用的字节序是不同的
                 比如:ubuntu采用就是小端序
                       计算机网络中传递的数据采用的是大端序存储
             大端序:数据的高字节存放在低地址,低字节存放在高地址
             小端序:数据的高字节存放在高地址,低字节存放在低地址   
             主机字节序:指的就是小端序
             网络字节序:指的就是大端序
 
            3.1 小端序ip转换成大端序ip
                     #include
                     #include
                     #include
                       in_addr_t inet_addr(const char *cp);
                              返回值:返回转换得到的大端序(网络字节序)ip
                                参数:cp --》字符串ip(小端序ip/主机字节序)
                       int inet_aton(const char *cp, struct in_addr *inp);
                              返回值:成功 0  失败 -1
                                参数:cp --》字符串ip
                                      inp --》存放转换得到的大端序ip
                          代码参考:inet_aton("192.168.22.9",&(bindaddr.sin_addr));

            3.2 小端序端口号转换成大端序端口
                     uint16_t htons(uint16_t hostshort);
                            h -->host  n -->network   s -->short
                              返回值:返回转换得到的大端序端口号
                                参数:hostshort --》小端序端口号

            3.3 大端序ip转换成小端序ip
                     char *inet_ntoa(struct in_addr in);
                              返回值:字符串格式的,小端序的ip
                                参数:in --》大端序ip

            3.4 大端序端口号转换成小端序端口
                     uint16_t ntohs(uint16_t netshort);

        (4)连接女朋友 --》连接服务器
             #include
             int connect(int socket, const struct sockaddr *address, socklen_t address_len);
                   返回值: 成功 0
                           失败 -1
                     参数:address --》存放对方(服务器)的ip和端口号

        (5)监听
             int listen(int socket, int backlog);
                   返回值:成功 0
                           失败 -1
                     参数:socket --》套接字文件描述符
                           backlog --》重点,同时最多允许多少个男朋友(客户端)连接我,一般这个值建议你5---10之间就可以了
                               listen(tcpsock,5); //最多允许5个男朋友(客户端)同时连接我(服务器)

        (6)愿意接听--》接受客户端的连接请求
             int accept(int socket, struct sockaddr *address,socklen_t *address_len);
                   返回值(重点,重点,重点): 成功 返回新的套接字文件描述符
                                            之所以要产生新的套接字,原因在于服务器需要区分不同的客户端
                                            每个客户端连接服务器成功之后,accept都会返回一个新的套接字
                                            失败 -1
                                      参数:socket --》套接字文件描述符
                                            address --》存放对方(客户端)的ip和端口号
                                            address_len --》地址长度大小,要求是指针
          总结验证accept函数的特点
              特点1:如果客户端没有连接服务器,服务器会一直阻塞在accept的位置
                     如果有客户端连接服务器成功,accept就不会阻塞,立马返回新的套接字文件描述符
              特点2:客户端连接成功,accept函数会自动帮你把连接成功的客户端信息(ip和端口号)保存到第二个参数中

        (7)收发信息
              tcp收发信息有两组函数都可以用来收发信息
                    第一组:传统的read/write
                    第二组:使用recv/send       read和recv作用相同   write和send作用相同
              发送信息
                    ssize_t send(int socket, const void *buffer, size_t length, int flags); 
                           返回值:成功 发送的字节数
                                   失败 -1
                             参数:前面三个参数跟write一模一样
                                   flags --》默认设置为0
              接收信息
                    ssize_t recv(int socket, void *buffer, size_t length, int flags); 
                           返回值:成功 接收的字节数
                                   0 --》对方断开连接了
                                   失败 -1
                             参数:前面三个参数跟read一模一样
                                   flags --》默认设置为0

遇到的问题
========================================
   1.ubuntu配置ip不熟
         ubuntu配置ip两种常用方法:
              方法一:使用图形用户界面配置
                      详细配置见截图
              方法二:修改配置文件配置
                      vim /etc/network/interfaces  打开这个文件
                      如果是静态ip,linux网络配置成桥接模式
                             (1) 打开/etc/network/interfaces文件
                                                在这个文件的后面加入如下几句话(静态)
                                                          auto  ens33
                                                          iface ens33 inet static      //设置静态ip
                                                          address 192.168.1.5   //设置ip地址
                                                          gateway 192.168.1.1  //设置网关
                                                          netmask 255.255.255.0  //子网掩码
                                                          dns-nameservers 192.168.120.1  //dns服务器 
                                                打开/etc/resolv.conf文件
                                                            namesever  你自己的DNS服务器地址                                        
                                                
                             (2)重启网络
                                               sudo /etc/init.d/networking force-reload     
                                               sudo /etc/init.d/networking restart   
                      如果是动态ip,linux的网络配置成NAT模式
                             (1) 打开/etc/network/interfaces文件
                                    在这个文件的后面加入如下几句话(动态)
                                                          auto  ens33
                                                          iface ens33 inet dhcp      //设置动态ip
                                                          后面的内容就不需要再写了

   2.绑定失败,地址还在使用这种错误
        问题一:
          Cannot assign requested address  
          原因:绝对是你的ip地址写错了
          解决方法:
               方法一:修改代码中的ip地址,改成正确的ip(啰嗦,每个拿到代码都要修改)
               方法二:linux提供了一个宏定义INADDR_ANY,配合一个函数htonl()函数实现自动配置本地主机上的任意ip地址--》自适应
                            函数原型:uint32_t htonl(uint32_t hostlong);
                               写法一:采用了具体的ip地址  bindaddr.sin_addr.s_addr=inet_addr("192.168.22.5");
                               写法二:没有采用具体的ip    bindaddr.sin_addr.s_addr=htonl(INADDR_ANY)
                  
        问题二:
          Address already in use 
          原因:ubuntu系统有个机制,当你的程序异常结束的时候,端口号并不会立马释放(大概会延迟30秒钟左右才释放),如果你性子急躁,马上再次运行程序,就会有这种错误提示
          解决方法:
               方法一:耐心等待30秒--》馊主意
               方法二:修改代码(主函数传参),换个端口号 --》略微好一点
               方法三:调用setsockopt()函数去设置套接字可以重复使用端口号
                        int setsockopt(int socket, int level, int option_name,const void *option_value, socklen_t option_len);
                            比如:
                                  int on=1;
                                  setsockopt(tcpsock,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
                            注意:此代码只能在socket之后,bind之前使用