网络编程套接字(二)

本文主要总结关于TCP网络编程以及实现;

一.TCP的网络编程:

传输层基于TCP协议进行网络通信编程:
1.传输流程:
服务端:
(1)创建套接字;
(2)绑定地址信息;
(3)开始监听:告诉系统可以接收客户端的连接请求;(这个过程中若有新的客户端连接请求到来,系统就会为这个客户端新建一个socket提供一对一服务;)

监听的接口:
int listen(int sockfd,int backlog);      

//其中backlog决定了内核中已完成对列的最大节点数量;为新客户端创建的socket在完成连接后都会放置到对列中;backlog决定了同一时间最大并发连接数;

listen在内核中创建两个对列:已完成连接对列,未完成连接对列;
socket为新的客户端在操作系统中重新建立一个新的socket;放在未连接对列中,当新建socket连接建立完成,就会将socket放到已完成连接对列中;当已完成对列的最大节点数量为backlog时,就不会新建socket,后来的客户端都将被丢弃;

(4)获取已经连接成功的客户端socket;=====>newsockfd

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

//从已完成对列中获取新建的客户端socket;获取之后就可以通过这个新建的socket与指定客户端进行通信;

客户端:
(1)创建套接字;
(2)绑定地址信息;(客户端不推荐手动绑定地址信息)
(3)向服务端发起连接请求;(客户端发起的连接请求必须在服务端建立连接之后,该请求为SYN请求,然后服务端也要向客户端进行回复并发送连接请求ACK+SYN(分为两部分:第一步是对客户端的请求做一个回复;第二步:为保证安全也同时给客户端发送连接请求);客户端再次进行回复;)

int connect (int sockfd,const,struct sockaddr *addr,socklen_t addrlen);
sockfd:套接字描述符;
addr:服务端的地址信息;
addrlen:地址信息的长度

(4)发送数据;

ssize_t send (int sockfd,const void *buf,size_t len, int flags);
sockfd:套接字描述符;
buf:要发送的数据;
len:要发送的数据长度;
flags:标志位;

(5)接收数据:

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

(6)关闭套接字:

close(fd);

原理图如下:
在这里插入图片描述
xshell通过22号端口向服务器发送请求;此时sshd监听22号端口,使xshell进行连接;(sshd是一个程序,它的内核中有一个socket结构体,结构体中对应了绑定信息,网卡拿到发往22号端口的信息都放到socket结构体中;在socket中一共有两个缓冲区:一个接收缓冲区,一个发送缓冲区)监听socket刚开始的时候监听的是22号端口,当有新的连接请求进入时,都会发送到接收缓冲区;此时操作系统会为新的客户端创建一个socket并进行一对一服务;
如果一瞬间有大量的套接字,大量的客户端同时给服务端发起请求,瞬间造成大量的资源请求;这样可能会导致资源耗尽,是非常危险的;因此要必须做出控制,限制同一时间所能创建的套接字的个数;
在这里插入图片描述
总结:如何连接请求:客户端发送请求给服务端,socket结构体为新的客户端在操作系统中重新建立了一个socket,端口依然是22号端口;此外还记录了客户端的地址信息;所以可以说为客户端重新创建了一个socket;

2.TCP简单编码程序:
(1)封装TCP的类:touch tcpsocket.hpp;

//使用C++封装TcpSocket类:
//创建套接字:
//bool socket();
//绑定地址地址信息:
//bool Bind(std::string &ip,uint16_t port);
//服务端开始监听:
//bool Listen(int backlog=5);
//客户端发起连接请求:
//bool connect(std::string &ip,uint16_t port);
//服务端获取已经连接完成的客户端新建socket:
//bool Accept (TcpSocket &clisock,struct sockaddr_in *addr);
//接收数据:
//bool Recv(std::string &buf);
//发送数据:
//bool Send(std::string &buf);
//关闭套接字:
//bool Close(); 

#include <string>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <iostream>
#include <string .h>
#define CHECK_RET(q) if((q)==false) { return -1;}
class TcpSocket{
pubilc:
   TcpSocket():_sockfd(-1){
   }
   void SetSockFd(int fd){
      _sockfd=fd;
   }
  
   bool Socket(){
      _sockfd=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
      if(_sockfd<0){
         perror("socket error");
         return false;
      }
      return true;
    }
 
    bool Bind(std::string &ip,uint16_t prot){
       struct sockaddr_in addr;
       addr.sin_family=AF_INET;
       addr.sin_port=htons(port);
       addr.sin_addr.s_addr=inet_addr(ip.c_str());
       socklen_t len=sizeof(struct sockaddr_in);
       int ret=bind(_sockfd,(struct sockaddr*)&addr,len);
       if(ret<0){
          perror("bind error");
          return false;
       }
       return true;
    }
   
    bool Listen(int backlog=10){
       //int listen(int sockfd,int backlog);
       //backlog:最大并发连接数-----内核中已完成连接队列的最大节点数;
       int ret=listen(_sockfd,backlog);
       if(ret<0){
          perror("listen error");
          return false;
       }
       return true;
    }
    
    bool Connect(std::string &ip,uint16_t port){
       //int connect (int sockfd,const struct sockaddr *addr,socklen_t arrdlen);
       //addr:要连接的服务端地址信息;
       struct sockaddr_in addr;
       addr.sin_family=AF_INET;
       addr.sin_port=htons(port);
       addr.sin_addr.s_addr=inet_addr(ip.c_str());
       socklen_t len=sizeof(struct sockaddr_in);
       int ret=connect(_sockfd,(struct sockaddr*)&addr,len);
       if(ret<0){
          perror("connect error");
          return false;
       }
       return true;
       }
       
       bool Accept(TcpSock &clisock,struct sockaddr_in *addr=NULL){
          //int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
          //addr:客户端的地址信息;
          //len:输入输出参数,既要指定接收长度,又要接收实际长度;
          //返回值:为新客户新建的socket套接字描述符;通过这个返回的描述符可以与指定的客户端进行通信;
          struct sockaddr_in _addr;
          socklen_t len=sizeof(struct sockaddr_in);
          int newfd=accept(_sockfd,(struct sockaddr*)&addr,&len);
          if(newfd<0){
             perror("accept error");
             return false;
          }
          if(addr!=NULL){
             memcpy(addr,&_addr,len);
          }
          
          csock.SetSockFd(newfd);
          //_sockfd ---仅用于接收新客户端连接请求;
          //newfd---专门用于与客户端进行通信;
          return true;
       }
  
       bool Recv(std::string &buf){
          char tmp[4096]={0};
          //ssize_t recv(int socket,void *buf,size_t len,int flags);  
          //flags: 0---默认阻塞接收; MSG_PEEK---获取数据,但是不从缓冲区移除;
          //返回值:实际接收到的数据长度;      失败:-1;        连接断开:0;
          int ret=recv(_sockfd,tmp,4096,0);
          if(ret<0){
             perror("recv error");
             return false;
          }
          else if(ret==0){
             printf("peer shutdown\n");
             return false;
          }
          buf.assign(tmp,ret);
          return true;
       }  
       
       bool Send(std::string &buf){
          //ssize_t send (int sockfd,const void *buf,size_t len, int flags);
          int ret=send(_sockfd,buf.c_str(),buf.size(),0);
          if(ret<0){
             perror("send error");
             return false;
          }
          return true;
        } 
private:
   int _sockfd;
};

面试题:如何快速判断连接是否已经断开???

原理:TCP的连接管理中,内建有保活机制;当长时间没有数据往来的时候,每隔一段时间都会向对方发送一个保活探测包,要求对方回复;当多次发送的保活探测包都没有得到回复响应时,说明连接已经断开;
若连接断开,则recv返回0(不是没有数据的意思,而是连接断开);因为TCP面向字节流,有可能收到半条数据;因此一定要对返回值做出判断,判断数据是否已经完全接收或完全发送;

(2)创建TCP服务端程序:touch tcp_srv.cpp;

/*创建套接字;
绑定地址信息;
开始监听;
获取已经链接成功的客户端地址信息;
接收数据;
发送数据;
关闭套接字;*/

#include <iostream>
#include "tcpsocket.hpp"
int mian(int argc,char *argv[]){
   if(argc!=3{
      printf("./tcp_srv ip port\n");
      return -1;
   }
   std::string ip=argv[1];
   uint16_t port=atoi(argv[2]);
   TcpSocket sock;
   CHECK_RET(sock.Socket());
   CHECT_RET(sock.Bind(ip,port));
   CHECK_RET(sock.Listen());
   while(1){
      TcpSocket clisock;
      struct sockaddr_in cliaddr;
      //accept是阻塞获取已经完成的连接;
      if(sock.Accept(clisock,&cliaddr)==false){
         continue;
      }
      printf("new connect client:%s:%d\n",inet_ntoa(cliaddr.sin_addr),ntohs(cliaddr.sin_port));
      std::string buf;
      clisock.Recv(buf);
      printf("client say:%s\n",buf.c_str);
      buf.clear();
      std::cout<<"srever say:";
      fflush(stdout);
      std::cin>>buf;
      clisock.Send(buf);
   }
   sock.Close();
   return 0;
 }

(3)创建TCP客户端程序:touch tcp_cli.cpp;

/*使用封装的TcpSocket类,实现tcp客户端程序;
创建套接字;
向服务端发起连接请求;
发送数据;
接收数据;
关闭套接字;*/

#include <iostream>
#include "tcpsocket.hpp"
int main(int argc,char *argv[]){
   if(argc!=3){
      std::cout<<"./tcp_cli ip port\n";
      return -1;
   }
   std::string ip=argv[1];
   uint16_t port=atoi(argv[2]);
   TcpSocket sock;
   CHECK_RET(sock.Socket());
   CHECK_RET(sock.Connect(ip,port));
   while(1){
      std::string buf;
      std::cin>>buf;
      std::cout<<"client say:";
      fflush(stdout);
      std::cin>>buf;
      sock.Send(buf);
      buf.clear();
      sock.Recv(buf);
      std::cout<<"server say:"<<buf<<std::cout;
   }
   sock.Close();
   return 0;
}

该程序只能实现一对一进行通信;基本的TCP服务端程序只能与一个客户端通信一次,无法实现与多个客户端进行通信;

本质原因:因为不知道客户端数据到底什么时候到来,因此写死的程序很容易造成阻塞;
(1)不知道客户端的请求什么时候来;
(2)不知道客户端的数据什么时候来;

如何才能实现一对多进行通信????
如果要解决这个问题目前一个执行流是不能够解决的,一个执行流只能让一个流程往下走;因此就要设计多个执行流进行解决即------多进程或多线程;

二.多进程版本的TCP服务端程序(实现多对一通信):

创建多进程版本的服务端:tcp_process.cpp;

#include <iostream>
#include <signal.h>
#include <sys/wait.h>
#include "tcpsocket.hpp"
void sigcb(int no){
   while(waitpid(-1,NULL,WNOHANG)>0);      //不需要等待子进程退出循环,父进程就可返回;
}
int mian(int argc,char *argv[]){
   if(argc!=3{
      printf("./tcp_srv ip port\n");
      return -1;
   }
   signal(SIGCHLD,sigcb);     //自定义一个子进程的处理方式;
   std::string ip=argv[1];
   uint16_t port=atoi(argv[2]);
   TcpSocket sock;
   CHECK_RET(sock.Socket());
   CHECT_RET(sock.Bind(ip,port));
   CHECK_RET(sock.Listen());
    while(1){
      TcpSocket clisock;
      //accept是阻塞获取已经完成的连接;
      if(sock.Accept(clisock,&cliaddr)==false){
         continue;
      }
      printf("new connect client:%s:%d\n",inet_ntoa(cliaddr.sin_addr),ntohs(cliaddr.sin_port));
      int pid=fork();
      if(pid==0){
         while(1){
            std::string buf;
            clisock.Recv(buf);
            printf("client say:%s\n",buf.c_str);
            buf.clear();
            std::cout<<"srever say:";
            fflush(stdout);
            std::cin>>buf;
            clisock.Send(buf);
         }
      }
      clisock.Close();    //只让子进程聊天,父进程把他关掉;
      //父进程专门接收客户端请求;
   }
   sock.Close();
   return 0;
}
    

三.多线程版本的TCP服务端程序(实现多对一通信):

创建多线程版本服务端:tcp_thread.cpp;

#include <iostream>
#include <pthread.h>
#include "tcpsocket.hpp"
void *thr_start(void *arg){
   TcpSocket *sock=(TcpSocket*)arg;
   while(1){
       std::string buf;
      sock->Recv(buf);
      printf("client say:%s\n",buf.c_str);
      buf.clear();
      std::cout<<"srever say:";
      fflush(stdout);
      std::cin>>buf;
      sock->Send(buf);
   }
   sock->Close();
   delete sock;
   return NULL;
 }
int mian(int argc,char *argv[]){
   if(argc!=3{
      printf("./tcp_srv ip port\n");
      return -1;
   }
   std::string ip=argv[1];
   uint16_t port=atoi(argv[2]);
   TcpSocket sock;
   CHECK_RET(sock.Socket());
   CHECT_RET(sock.Bind(ip,port));
   CHECK_RET(sock.Listen());
   while(1){
      TcpSocket *clisock=new TcpSocket();     //数据段共享;
      struct sockaddr_in cliaddr;
      //accept是阻塞获取已经完成的连接;
       if(sock.Accept(*clisock,&cliaddr)==false){
         continue;
      }
      printf("new connect client:%s:%d\n",inet_ntoa(cliaddr.sin_addr),ntohs(cliaddr.sin_port));
      pthread_t tid;
      pthread_creat(&tid,NULL,thr_start,(void*)&clisock);   ///创建线程入口函数;
      pthread_detach(tid)//多线程中主线程不能关闭socket,因为线程之间共享线程描述符表,如果在主线程中关闭socket,其他线程中的这个描述符表也就关闭了;但多进程可以,因为多进程中数据是独有的;
   }
   sock.Close();
   return 0;
 }
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值