2-4:套接字(Socket)编程之TCP通信

Posted 快乐江湖

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2-4:套接字(Socket)编程之TCP通信相关的知识,希望对你有一定的参考价值。

一 TCP通信服务端和客户端——和UDP区别

TCP是面向字节流的,是有连接的,会在服务端和客户端之间建立一条连接,而UDP显得就比较简单,只负责传递。在2-3:套接字(Socket)编程之UDP通信这一节详细叙述了UDP通信及套接字相关内容,本节TCP通信将会在上节的基础上,对TCP和UDP中代码的不同部分做以补充。

(1)服务端

tcpServer.h

#include <iostream>
#include <string>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
using namespace std;

#define  BACKLOG 5

class tcpServer
{
private:
  int _port;
  int listen_sock;
public:
  tcpServer(int port=8080):_port(port)
  {}
  void initServer()
  {
    listen_sock=socket(AF_INET,SOCK_STREAM,0);//区别UDP,TCP采用流式套接字
    if(listen_sock < 0)
    {
      cout<<"套接字创建失败"<<endl;
      exit(2);
    }

    struct sockaddr_in local;
    local .sin_family=AF_INET;
    local.sin_port=htons(_port);
    local.sin_addr.s_addr=htonl(INADDR_ANY);

    if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local)) < 0)
    {
      cout<<"绑定失败"<<endl;
      exit(3);
    }
    //开启监听
   if(listen(listen_sock,BACKLOG) < 0)
   {
      cout<<"绑定失败"<<endl;
      exit(4);
   } 
  }

  //通信服务
  void Service(int sock)
  {
      char buffer[1024];
      while(1)
      {
        size_t s=recv(sock,buffer,sizeof(buffer)-1,0);//区别UDP
        if(s > 0)
        {
          buffer[s]='\\0';
          cout<<"服务端收到客户端的消息:"<<buffer<<endl;

          send(sock,buffer,strlen(buffer),0);//区别UDP
        }
        if(s==0)//客户端下线,服务端收到0
        {
          cout<<"客户端已经下线"<<endl;
          close(sock);//注意关闭套接字,资源是有限的
          break;
        }
      }
  }
  
  void startServer()
  {
    sockaddr_in endpoint;
    while(1)
    {
      //TCP-accept
      socklen_t len=sizeof(endpoint);
      int _sock=accept(listen_sock,(struct sockaddr*)&endpoint,&len);
      if(_sock < 0)
      {
        cout<<"accept失败"<<endl;
        continue;
      }
      cout<<"一台新的客户端已经连接"<<endl;
      //拿到套接字进行通信
      Service(_sock);
    }
  }
  ~tcpServer()
  {
    close(listen_sock);
  }
};


1:创建套接字

相较于UDP,TCP通信时选择的套接字是流式套接字

listen_sock=socket(AF_INET,SOCK_STREAM,0);

2:listen监听
在套接字绑定之前,UDP和TCP基本是一致的。在TCP通信中,有两套套接字,其中一套用于监听,称之为监听套接字。TCP不同于UDP,如果和客户端之间没有连接就不能发送数据,所以要把一个套接字设置为监听状态,以便在任意时候客户端请求服务器时都能有套接字与该客户端建立连接。关于BACKLOG选项后序再网络原理里面再做解释

函数原型如下

 #include <sys/socket.h>
 int listen(int s, int backlog);

3:accept

accept表示服务端接受一个连接,每当一个客户端连接成功时,就会建立一条连接。最为关键的是该接口的返回值,其返回值也是一个套接字,前面说过TCP通信中存在两套套接字,一个就是刚才说到的过的监听套接字,它的职责就是不断从网络中获取连接,因为可能会用很多客户端想要和服务端通信,当它把连接拿上来之后,调用接口accept,其返回值所产生的套接字就是专门用来处理这条连接,用于进行通信的。这样做的话整个服务端只需要一个监听套接字,同时连接只要成功,只需调用接口产生新的套接字再用于通信即可。

 int _sock=accept(listen_sock,(struct sockaddr*)&endpoint,&len);

当accept失败时,该接口返回值也会小于0,需要注意的是此时只是一个连接失败了,你不能因为这么一个连接失败了,而把整个服务器给退了,所以要继续continue。
当accept成功时,我们建立一个新的函数,该函数就是用来专门去处理通信问题的

如下在accept成功之后,加入这样一句代码,表示连接上了一台新的客户端
在这里插入图片描述
然后使用telnet命令,如果你的Centos没有这个命令,需要进行一定配置,链接如下,亲测有效

CentOS 7.4安装telnet服务端

然后使用本地环回测试,使用telnet进行连接,可以发现当一台主机连接成功时,服务端提示出了相应的讯息
在这里插入图片描述
在这里插入图片描述

4:recv和send
不同于UDP中的recvfrom和sendto,在TCP通信中,我们尽可能采用的是recv和send来接受和发送

#include <sys/types.h>
#include <sys/socket.h>

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
int send(int s, const void *msg, size_t len, int flags);

(2)客户端

tcpClient.h

#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
using namespace std;

class tcpClient
{
private:
  string _ip;//服务端IP和端口号
  int _port;
  int _sock;
public:
  tcpClient(string ip="127.0.0.1",int port=8080):_ip(ip),_port(port)
  {}

  void initClient()
  {
    _sock=socket(AF_INET,SOCK_STREAM,0);//区别UDP
    if(_sock < 0)
    {
      cout<<"套接字创建失败"<<endl;
      exit(2);
    }

    struct sockaddr_in sev;
    sev.sin_family=AF_INET;
    sev.sin_port=htons(_port);
    sev.sin_addr.s_addr=inet_addr(_ip.c_str());

	//TCP-connect
    if(connect(_sock,(struct sockaddr*)&sev,sizeof(sev))!=0)
    {
      cout<<"connect失败"<<endl;
      exit(3);
    }
  }

  void startClient()
  {
      char mssage[64];
      while(1)
      {   
        size_t s=read(0,mssage,sizeof(mssage)-1);//从标准输入读入
        if(s > 0)
        {
          mssage[s-1]='\\0';//剔除换行符
          send(_sock,mssage,strlen(mssage),0);
          
          ssize_t ret=recv(_sock,mssage,sizeof(mssage)-1,0);
          if(ret > 0)
          {
            cout<<"客户端得到服务端消息"<<mssage<<endl;
          }
        }
      }
  }

  ~tcpClient()
  {

    close(_sock);
  }


};

1:connect

TCP是面向连接,因此对于客户端来说,它就要调用connect接口连接服务端,如果返回值为0表示连接成功

connect(_sock,(struct sockaddr*)&sev,sizeof(sev))

2:客户端退出

当客户端退出时,我们要让服务器知道客户端退出,并且关闭已经打开的套接字
那么服务端如何知道客户端退出了呢,这其实和recv接口的返回值有关

These calls return the number of bytes received, or -1 if an error occurred. In the event of an error, errno is set to indicate theerror. The return value will be 0 when the peer has performed an orderly shutdown.

它的意思就说如果客户端下线,那么服务端将会接受到0 因此,服务端会有下面这样代码

if(s==0)
{
	cout<<"客户端已经下线"<<endl;
	close(sock);
	break;
}

在这里插入图片描述

二:TCP通信-多进程/线程

使用上面的代码,利用telnet进行测试,xshell中有多个窗口,代表多个客户端,第一个客户端连接后,服务端的确打印出了相关讯息,但是第二个和第三个在连接时却没有打印出信息
在这里插入图片描述

问题的原因就是咋们编写的服务器目前是一个单进程版的服务器,当第一个主机连接时,由于没有发送数据,因此进程会被阻塞在service函数中,到时后面的客户端连接不上

(1)多进程版本

tcpServer.h

#include <iostream>
#include <signal.h>
#include <string>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
using namespace std;

#define  BACKLOG 5

class tcpServer
{
private:
  int _port;
  int listen_sock;
public:
  tcpServer(int port=8080):_port(port)
  {}
  void initServer()
  {
    signal(SIGCHLD,SIG_IGN);//让子进程自己销毁
    
    listen_sock=socket(AF_INET,SOCK_STREAM,0);//区别UDP,TCP采用流式套接字
    if(listen_sock < 0)
    {
      cout<<"套接字创建失败"<<endl;
      exit(2);
    }
    struct sockaddr_in local;
    local .sin_family=AF_INET;
    local.sin_port=htons(_port);
    local.sin_addr.s_addr=htonl(INADDR_ANY);
    if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local)) < 0)
    {
      cout<<"绑定失败"<<endl;
      exit(3);
    }
    //开启监听
   if(listen(listen_sock,BACKLOG) < 0)
   {
      cout<<"绑定失败"<<endl;
      exit(4);
   }
  }
  void Service(int sock)
  {
      char buffer[1024];
      while(1)
      {
 
        size_t s=recv(sock,buffer,sizeof(buffer)-1,0);
        if(s > 0)
        {
          buffer[s]='\\0';
          cout<<"服务端收到客户端的消息:"<<buffer<<endl;

          send(sock,buffer,strlen(buffer),0);
        }
        if(s==0)
        {
          cout<<"客户端已经下线"<<endl;
          close(sock);
          break;
        }
      }
  }
  
  void startServer()
  {
    sockaddr_in endpoint;
    while(1)
    {
      //accept
      socklen_t len=sizeof(endpoint);
      int _sock=accept(listen_sock,(struct sockaddr*)&endpoint,&len);
      if(_sock < 0)
      {
        cout<<"accept失败"<<endl;
        continue;
      }
      string client_info=inet_ntoa(endpoint.sin_addr);
      client_info+=":";
      client_info+=to_string(ntohs(endpoint.sin_port));

      cout<<"一台新的客户端已经连接:"<<client_info<<endl;
      
      pid_t id=fork();
      if(id==0)//子进程
      {
        close(listen_sock); //子进程会以父进程为模板,复制父进程PCB,所以对于子进程来说,可以关闭的它的listen_sock,尽量节省资源
        Service(_sock);
        exit(0);//子进程处理完毕
      }
      //对于父进程它只关心监听套接字,所以可以把父进程的sock关闭,而且是必须关掉,因为这个sock对它没用了
      close(_sock);
    }
  }
  ~tcpServer()
  {
    close(listen_sock);
  }
};

以上代码中关于父子进程之间的关系,以及进程等待这里就不细谈了,详见

Linux系统编程

再次强调,对于子进程,它可以关闭监听套接字,因为子进程是用来通信的,它只关心sock,对于父进程则必须要关闭sock,只保留监听套接字,否则客户端连接的越多,系统资源将会被耗尽

再次测试,可以发现多个客户端可以同时连接服务器
在这里插入图片描述

(2)多线程版本

在Linux系统编程那一部分我们详细说过多线程和多进程的优缺点,创建进程的代价远远高于创建线程,因此多线程版本如下

#include <iostream>
#include <pthread.h>
#include <signal.h>
#include <string>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
using namespace std;

#define  BACKLOG 5

class tcpServer
{
private:
  int _port;
  int listen_sock;
public:
  tcpServer(int port=8080):_port(port)
  {}
  void initServer()
  {
    
    listen_sock=socket(AF_INET,SOCK_STREAM,0);//区别UDP,TCP采用流式套接字
    if(listen_sock < 0)
    {
      cout<<"套接字创建失败"<<endl;
      exit(2);
    }
    struct sockaddr_in local;
    local .sin_family=AF_INET;
    local.sin_port=htons(_port);
    local.sin_addr.s_addr=htonl(INADDR_ANY);

    if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local)) < 0)
    {
      cout<<"绑定失败"<<endl;
      exit(3);
    }
    //开启监听
   if(listen(listen_sock,BACKLOG) < 0)
   {
      cout<<"绑定失败"<<endl;
      exit(4);
   }
  
  }
  static void Service(int sock)
  {
      char buffer[1024];
      while(1)
      {
        size_t s=recv(sock,buffer,sizeof(buffer)-1,0);
        if(s > 0)
        {
          buffer[s]='\\0';
          cout<<"服务端收到客户端的消息:"<<buffer<<endl;

          send(sock,buffer,strlen(buffer),0);
        }
        if(s==0)
        {
          cout<<"客户端已经下线"<<endl;
          close(sock);
          break;
        }
      }
  }
  
  static void* Route(void* args)//线程路线
  {
    pthread_detach(pthread_self());
    int* p=(int*)args;
    int sock=*p;
    Java网络编程之TCP网络编程

网络编程之Socket & ServerSocket

python之socket编程

TCP/IP之socket编程

网络编程之套接字socket

基于TCP协议之——socket编程