基于tcp的C/S模型

基于TCP/ip模型下的c/s交互模型

tcp网络通信的小知识

  1.因为tcp是面向连接的,所以在写基于tcp服务器的代码时,要有listen套接字和accept套接字,而基于udp模型的代码,并且udp客户端直接调用 recvfrom/sendto 直接通信即可,不用调用connect函数,这也分别体现出了它们的特性tcp面向连接,而udp则是无需连接。
  2.对于read在网络通信中,因为tcp是基于字节流的,所以每次read上来的数据都是一个数据段。可能你这里发了一个1024字节的数据,到了运输层可能分段,所以对端可能不会一次性对上来1024字节的数据。所以由此看出来read每次读socket的文件的时候,都读的是一个数据段。
  3.对于阻塞socket而言,write调用的时候,当我们把应用层的数据拷贝到内核缓冲区的时候,如果内核缓冲区已满,那么就会阻塞,从write 函数返回也并不代表,数据发送到了对端,只是代表了数据已经拷贝到了内核缓冲区。
  4.对于UDP来说,因为是不可靠的,所以也就udp socket也就没有内核缓冲区,网络层加上包头后直接把数据发送到 数据链路层 的 队列中。从write 返回就是代表数据报已经写入到了数据链路队列。如果 我们发的应用层数据大于 SO_SNDBUF(套接字发送缓冲区上限) 会收到 EMSGSIZE 错误,这不像tcp 会阻塞。
  如果链接队列满了,因为是udp 协议栈向上报错,一直到发送方。udp 并不会重传,所以内核会向应用层返回一个 ENOBUFS 。
  5.内核的TCP发送缓冲区会一直缓存从应用层拷贝到的数据,一直到收到ACK后,才把这些数据丢弃。
  6.write成功返回,对于 TCP来讲只是应用层数据被拷贝到了内核发送缓冲区中,对于 UDP来讲只是数据被加入到了 数据链路层队列中。
  7.connect调用失败后,并不可以直接重用该socket,重用前需 close再用,因为基于 Tcp状态转换图得知 , connect 函数其实就是三次握手,那么当三次握手失败的话,socket 不是处于 CLOSED 而是SYN_SENT状态,所以我们必须重新关闭才能再次使用该 socket。
  8.对于sockaddr 结构体的疑问,为什么这些socket api 不直接使用void* , 而使用这个通用结构体,因为socket api 比 c 语言的 void * 更早出世,所以在当时没有 void* 这一概念的时候就使用了通用结构体来充当void*。

tcp数据流

这里写图片描述
1. 应用层把数据发送给TCP.
2. TCP根据 MSS大小把数据分为一个个的数据段(SO_SNDBUF是发送缓冲区大小)
3. IP层加上ip 头部后转发给相对应的数据链路层(可能这里会分片)
4. 数据链路层有输出队列,如果输出队列满了,逆向数据流向上发送错误,收到错误后TCP会过段时间再发一次数据。这些情况都是内核实现的,应用根本不知道传送的情况。

tcp_server

首先我们需要先有个套接字,这个套接字必须绑定服务器相应的ip地址和port端口号。而且这个套接字需要是listen状态的。那么当有client向tcp发送连接时,服务器进程调用accept函数就可以查看listen的未决连接队列是否有未处理的连接,如果有就创建一个相应cilent连接的套接字返回,这时我们就可以通过这个accept套接字和cilent通信了。

步骤

socket调用
bind 绑定服务器ip和端口号
listen 使该套接字成为监听状态
accept 调用拿到与相应cilent绑定的套接字与之通信。

tcp_cilent

cilent的原理就很简单了,首先调用socket 申请个套接字,然后调用connect连接server的ip/port,connect返回的这个套接字我们就可以与server通信了。

模型中使用的函数

int socket(int domain,int type,int protocl)

第一个参数 指网络层是什么类型的协议, ipv4 ipv6 之一类。
第二个参数 指传输的数据流的类型,tcp 为 sock_stream (udp为sock_DGRAM,可见udp传输的是数据报,不可靠传输)
第三个参数 基于1/2参数选项组合来选的,一般设置为0,让内核为我们选择

int bind(int sockfd,const struct * sockaddr,socklen_t addrlen)

第一个参数为 需要绑定的套接字
第二个参数为 关于socket地址的数据结构,它其中记录了套接字要使用到的ip/port,所以定义出这个结构体就可以跟我们申请的套接字绑定了。(一般网络协议不同套接字地址结构体不同,调用时需要去强转)
第三个参数为 第二个参数类型的大小。
它的返回值很特殊,成功返回0。

bind中的IP地址

  对于Client端来说,如果我们绑定了IP表明,这个IP是它的源IP。对于Server端来讲,绑定了IP表明 Server只能接受这个IP上的连接(也就是固定网卡接口了)。
  如果我们不自己设置,内核也会为我们设置。让内核来选择随机端口的话,我们只需设置端口号为0即可。对于让内核来选IP地址则,如果是IPV4给其赋值 INADDR_ANY 即可,也就是0。对于IPV6,则需要赋值 in6addr_any 这个是结构体,不过它值也是0,但是我们不能直接用0赋值。
  内核选择IP地址的时机是当有一个连接Connect(TCP)也就是三次握手完毕后或者 当一个UDP数据报被 发出去(UDP), 此时内核才会为socket绑定地址。
  选择IP地址的方式对于Tcp来说,如果是一个Server(listen态socket),内核是这样的,根据Client端发来的 SYN段中的目的端地址作为 源IP。如果是一个Client,内核会根据要连接的server的路由情况,从各个网卡中选择一个合适的IP地址。

bind中的端口号 (第二个参数为const的缘故不能直接查看ip与port)

  当我们设置端口号为0的时候,内核会为我们选择一个随机端口号,由于第二个参数是const的原因,所以调用完毕后,我们无法查看内核为我们选择的端口号,我们只能通过getsocketname来查看。
  绑定知名端口号需要root权限。

bind绑定相同的地址

  有时我们不想Server主动断开连接后由于time_wait,而不能立即重启,所以就想重新绑定相同的地址的Server。
  那么为了端口复用需满足以下条件其中之一:

  1. socket绑定的不是同一网卡可以绑定
  2. 设置了 SO_REUSEADDR并且不能是LINTEN状态的节点
    可见如果是处于time_wait的节点,我们只需提前设置 SO_REUSEADDR即可端口复用,解决Server不能立刻重启。
将ip 与 port 设置为0

  当我们设置0,并非内核中对应的连接节点的ip与port就是0,只是代表让内核帮我们选择合适的 ip 与 port 来绑定。

int listen (int sockfd,int backlog)

第一个参数 绑定后的套接字
第二个参数 listen_socket 的 有俩个队列。完全链接队列(状态为ESTABLEISHED)与 半完全链接队列。backlog 参数是用来设置完全连接队列大小的参数,如果设置为0 ,根据不同平台其完全连接队列的值不一样,所以不想监听关闭该socket即可。
历史上backlog参数是设置这完全与非完全连接队列的大小,但是由于黑客的Syn攻击,导致非法连接占满了完全连接队列,导致正常客户的请求无法连接进来,所以为了防止SYN攻击,内核会作一些设置来防护,所以内核来设置半完全连接队列防止syn攻击,而具体接受多少连接数目由程序员来设定,所以backlog参数在之后就变为只是设置完全连接队列的大小的参数了。

链接队列

半完全链接队列大小,可以通过系统参数 tcp_max_syn_backlog来决定默认32位下512,64位下2048,最大8192。(测试环境是 Centos 6.5 32/64位)
完全链接队列大小,/proc/sys/net/core/somaxconn 来设置 , 最终大小为 1.5*backlog 和 内核参数俩个当中的最小值。

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

该函数是Tcp所使用的函数。该函数主要从上面所提到的完全链接队列中,提取完全链接队列头部的节点(pop ),如果完全链接队列为空则阻塞(默认是sockfd阻塞套接字)。
第一个参数 为listen态的套接字(必须是调用完bind 和 listen 的套接字)
第二个参数 为一个通用地址结构体,它主要用来返回远端 Client 的地址结构体(主要为输出型参数,直接定义直接传就行不用像bind还需相应的赋值)
第三个参数 为对应远端Client 的地址结构体大小。
如果不关心Client的信息,第二个与第三个设置为NULL。
对于服务器来讲,如果服务完这个Client后,需要把这个套接字close掉。

返回值

这个函数有三个返回值,第一个是int 它要么代表新连接的socket 或者 一个错误状态,第二个返回值是 远端Client的地址结构体,第三个参数是地址结构体的大小。

该结构体在 这个#include <netinet/in.h>头文件中
struct sockaddr_in

{

short sin_family;/*Address family一般来说AF_INET(地址族)PF_INET(协议族)*/

unsigned short sin_port;/*Port number(必须要采用网络数据格式,普通数字可以用htons()函数转换成网络数据格式的数字)*/

struct in_addr sin_addr;/*IP address in network byte order(Internet address)*/

unsigned char sin_zero[8];/*Same size as struct sockaddr没有实际意义,只是为了 跟SOCKADDR结构在内存中对齐*/

};
typedef uint32_t in_addr_t;
struct in_addr
  {
    in_addr_t s_addr;
  };

终止连接函数

int close(int socketfd)

  首先如果我们要正常终止连接的话就需要调用close函数,调用时只有对应的socketfd 对应的文件描述符对应的 file struct 结构体的引用计数为1的时候,调用才会触发正常的四次挥手否则只是引用计数减1.
  如果Tcp的发生缓冲区还有数据,调用close函数后立即返回,这时候内核会把发送缓冲区的数据发送出去但是并不会对这些数据进行ACK,随即就会发送FIN段进行连接终止。
  如果想要确保对端收到这些数据,需要设置SO_LINGER选项,下面是不同设置了SO_LINGER选项调用 close 函数的结果。

struct linger { 
     int l_onoff;  开关
     int l_linger; 秒数
};

  1.l_onoff 设置为0 表明关闭该选项即进行默认close操作,调用后立即返回。
  2.l_linger 设置为0,但是 l_onoff 非0,表明开启linger选项,但是立即终止连接,内核会丢弃所有TCP发生缓冲区的数据,并且使用异常的RST包来快速终止连接,对端收到RST包后会立即关闭连接。(RST包表示连接发生异常,列如当收到了一个非法序列号的包时就会发送RST异常终止连接,对端收到RST会直接终止连接。)
  这样设置可以避免主动关闭时进入的Time_wait状态,但缺点是快速建立相同的连接的时候即在2MSL内时,如果原链接上有旧的重发的包的话,则会导致新链接检验序列号是发现是非法序列号,导致新链接异常关闭。
  3.俩个都非0,这个就讲究了
  如果是阻塞套接字的话,close 就不会立即返回,这个时候会阻塞,除非有接下来俩个事情中的其中之一发生:
  1.超时
  2.发生缓冲区 的数据得到了ACK
  如果是非阻塞套接字,上面俩个条件都未就绪返回EAGAIN。
  无论套接字是否阻塞,在超时的时间内成功收到ACK,则close 返回0表成功,否则返回EWOULDBLOCK,然后发生缓冲区的数据会被丢弃。

int shutdown( int sockfd , int howto)

  SHUT_WR 保证发生缓冲区的数据一定对端应用层读取到了,并且终止连接。

几种不同方式结束连接实际的图示

默认close 立即返回

正常close

开启 设置超时时间为0秒

这里写图片描述

开启linger 但是设置时间非0 成功调用

这里写图片描述

开启但是超时导致调用失败

这里写图片描述

shutdown SHUT_WR 关闭

这里写图片描述

总结

  首先设置为0和超时都最终以rst结束连接,主动断开方都不会进入time_wait。其次注意设置了LINGER选项成功调用与shutdown函数的区别,一个是收到 ack data and fin 返回一个是 收到 对端 FIN 返回,这俩个代表的意义有重大不同。第一个只代表对端机器确切收到了数据包,第二个不只收到了数据包,对端应用层已经读取了该数据。
  故设置并开启了SO_LINGER选项并且设置了超时时间,成功返回。表明对端内核TCP接受缓冲区肯定收到了数据,但是对应应用读取了数据没有,这个我们是不知道的。
  如果想确保对端应用层接收到了数据再断开连接有俩个方法
  1.应用层 ack
  2.shutdown函数并设置SHUT_WR选项。

附属函数

ip数值与 点分ip字符串转换函数
in_addr_t inet_addr(const char *ip); 
将点分四字节ip地址的字符串转为网络ip地址
char * inet_ntoa(struct in_addr in); 
将网络ip地址转为点分四字节ip地址的字符串注意的是 inet_ntoa 是不可重人的函数(因为它
把返回的结果保持在同一个静态的变量中)所以用多个临时变量去纪录 inet_ntoa的结果是不可
取的,都指向同一个字符串,所以现在用inet_ntop去代替它。
主机序转换为网络序函数

       uint32_t htonl(uint32_t hostlong);

       uint16_t htons(uint16_t hostshort);

       uint32_t ntohl(uint32_t netlong);

       uint16_t ntohs(uint16_t netshort);

       这四个函数是用来将我们在代码内定义的数字转为网络号,因为从命令行拿到的端口号
       不能直接使用,或我们函数内部定义关于端口号的变量不能直接使用,需要转换后才能
       使用。

该模型下的三次握手与四次挥手

握手

           C                                   S
conect 调用,客服端的操作系统发送SYN 请求连接 后调用进程阻塞       服务器收到后回应SYN-ACK(确定收到SYN)
客服端进程收到SYN-ACK后,再次回应ACK同时客户端进程从connect返回    服务器收到后三次握手结束,从accept退出

挥手

 C主动调用close关闭socket文件描述符,此时客服端的OS发送FIN数据段和EOF向服务器。 
 S收到了FIN数据段进入close_wait状态并向C发送ACK数据段并且系统通知上层应用程序等到应该调用close函数
 S上层应用程序调用read时返回为0时代表操作系统收到了FIN数据段,调用close函数,系统向C发生FIN数据段,进入last_ack状态
 C的操作系统收到FIN数据段后,回复ACK数据段,此时C进入time_wait状态,当S收到ACK后,S的套接字关闭。
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                                            四次挥手结束

TIME_WAIT

  通信双方建立TCP连接后,主动关闭连接的一方就会进入TIME_WAIT状态。当主动关闭连接时,主动关闭方会发送最后一个ack后,然后会进入TIME_WAIT状态,再停留2个MSL时间,进入CLOSED状态。(MSL:IP数据报能在互联网上最长的生命周期 )
   因为TCP协议在关闭连接的四次握手过程中,最终的ACK是由主动关闭连接的一端(后面统称A端)发出的,如果这个ACK丢失,对方(后面统称B端)将重发出最终的FIN。因此A端必须维护状态信息(TIME_WAIT)允许它重发最终的ACK。(即若A端的FIN丢失了,要确保B端可以确认这个重发的FIN包而需发出最后这一个ACK的包,所以要有TMIE_WAIT的存在)。
   因为TIME_WAIT的存在所以,客户端或服务器主动断开后不能立刻重启相应端口的服务。这对客服端没什么,但是对于服务器是致命的打击。假设一个大公司的服务器突然断了,不能及时重连要进行TAME_WAIT等2倍的MSL的时间,这可能损失惨重。所以可以主动设置为断开连接时设置为RST方式,当关闭时发送RST段直接断开,不用进行TIME_WAIT.(当然接受方接收到RST会解释成一个错误),如何设置上面SO_LINGER的第二个场景有讲,还有一个方法避免Time_wait , 使用SO_REUSEADDR选项,下面列子中有。
   SO_REUSEADDR选项不可以对listen socket使用,即使linstensocket使用SO_REUSEADDR,再启动一个进程对其进行重绑会显示失败。

基于tcp的C/S模型的多线程版代码

server

#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<string.h>
#include <pthread.h>
int startup(const char*ip,const char* port)
{
    int sock=-1;
    if((sock=socket(AF_INET,SOCK_STREAM,0))==-1)
    {
      perror("socket");
      exit(1);
    }
    int opt = 1;
    setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));//设置端口复用,服务器主动断开不在time_wait
    struct sockaddr_in sock_addr;
    socklen_t len=sizeof(sock_addr);
    sock_addr.sin_family = AF_INET;
    sock_addr.sin_port= htons(atoi(port));
    sock_addr.sin_addr.s_addr= inet_addr(ip);
    if((bind(sock,(const struct sockaddr * )&sock_addr,len))<0)
     {
      perror("bind");
      exit(2);
     }
    if((listen(sock,0))<0){
      perror("listen");
      exit(3);
     }
    return sock;
}
void usesage(const char *p)
{
   printf("correct : %s ip  port",p);
}
void* Accept(void * arg)
{
    int answer_sock=(int)arg;
       char buf[1024];
       memset(buf,0,1024);
        ssize_t s;
      while(1)
      {
     if((s=read(answer_sock,buf,sizeof(buf)))>0)
       {
           buf[s]=0;
           printf("cilent %s",buf);
           printf("please Enter:");
           fflush(stdout);
           s=read(0,buf,sizeof(buf));
           answer_sock=(int)arg;
           write(answer_sock,buf,s); //这里大小为S特别重要,之前我一直写的是sizeof(buf),结果输出老出错
       }
       else if(s==0)
      {
        printf(" cilent quit\n");
        break;
      }
      else
      {
        perror("read");
        pthread_exit((void*)1);
      }
     }
     close(answer_sock);
     return (void*)1;
}
int main(int argvs,const char*arg[])
{
     if(argvs!=3)
      {
        usesage(arg[0]);
        exit(4);
      }
     int listen=startup(arg[1],arg[2]);
     struct sockaddr_in server;
     socklen_t len=sizeof(struct sockaddr_in);
     while(1)
    {
     int answer_sock=accept(listen,(struct sockaddr *)&server,&len);
     printf(" yeah cilent success!\n");
     pthread_t tid;
     pthread_create(&tid,NULL,Accept,(void*)answer_sock);
     pthread_detach(tid);
    }
   close(listen);
   return 0;
}

cilent

#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<string.h>
int main(int argv,const char*arg[])
{
   int req_sock=socket(AF_INET,SOCK_STREAM,0);
   struct sockaddr_in sock_in;
   sock_in.sin_family=AF_INET;
   sock_in.sin_port=  htons(atoi(arg[2]));
   sock_in.sin_addr.s_addr=inet_addr(arg[1]);
   socklen_t len=sizeof(sock_in);
   connect(req_sock,(struct sockaddr*)&sock_in,len);
   char buf[1024];
   memset(buf,0,1024);
   while(1)
   {
     printf("please Enter:");
     fflush(stdout);
     ssize_t s=read(0,buf,sizeof(buf));
      buf[s]=0;
     s=write(req_sock,buf,s);//这里大小为S特别重要,之前我一直写的是sizeof(buf),结果输出老出错
     if(s<0)
      {
      perror("write");
      exit(1);
     }
     s=read(req_sock,buf,sizeof(buf));
     if(s>0)
     {
       buf[s]=0;
       if(strcmp(buf,"quit")==0)break;
       printf("server say:%s",buf);
       fflush(stdout);
     }
   }
   return 0;
   close(req_sock);
}

验证结果

这里写图片描述
这里写图片描述
这里写图片描述

猜你喜欢

转载自blog.csdn.net/sdoyuxuan/article/details/74616427