【Linux】socket套接字编程----TCP

TCPsocket

  • 流程示意图
  • 接口
    • 1. 创建套接字
    • 2. 为套接字绑定地址信息
    • 3. 服务端开始监听
        • `backlog参数`
    • 4. 获取新建socket的操作句柄
    • 5. 通过新获取的套接字操作句柄(accept返回的描述符)与指定客户端进行通信
    • 6. 关闭套接字
    • 7. 客户端向服务端发送连接请求
  • 注意点
    • 问题
    • 解决方案
      • 多进程
      • 多线程
  • 代码实现
    • 封装Socket类
    • 客户端
    • 服务端
  • 套接字与管道
  • TCP和UDP套接字的区别
    • UDP
    • TCP

流程示意图

【Linux】socket套接字编程----TCP_第1张图片

接口

1. 创建套接字

int socket(int domain, int type, int protocol);
domain:地址域----不同的网络地址结构  AF_INET即IPv4地址域
type:套接字类型----流式套接字(SOCK_STREAM)/数据报套接字(SOCK_DGRAM)
protocol:使用协议  0表示不同套接字下的默认协议(流式套接字为TCP,数据报套接字为UDP)
		  IPPROTO_TCP -- TCP协议 IPPROTO_UDP -- UDP协议
返回值:套接字的操作句柄----文件描述符

2. 为套接字绑定地址信息

int bind(int sockfd, struct sockaddr *addr, socklen_t len);
sockfd:创建套接字返回的操作句柄
addr:要绑定的地址信息结构体
len:地址信息的长度
返回值:成功返回0,失败返回-1

3. 服务端开始监听

告诉操作系统开始接收连接请求

int listen(int sockfd, struct sockaddr *addr, socklen_t len);

backlog参数

  • backlog参数提示内核监听队列的最大长度。监听队列的长度如果超过backlog,服务器将不受理新的客户端连接,客户端也将收到ECONNREFUSED错误信息。在内核版本2.2之前的Linux中,backlog参数是指所有处于半连接状态(SYN_RCVD)和完全连接状态(ESTABLISHED)的socket上限。但自内核版本2.2之后,它只表示处于完全连接状态的socket上限。backlog的典型值是5,此时处于完全连接状态的socket最多能有6个(backlog + 1)。

4. 获取新建socket的操作句柄

从内核指定socket的pending queue中取出一个socket,返回操作句柄

int accept(int sockfd, struct sockaddr *addr, socklen_t *len);
sockfd:监听套接字----指定要获取 哪个pending queue中的套接字
addr:获取一个套接字,这个套接字与指定的客户端进行通信,通过addr获取这个客户端的地址信息
len:输入输出型参数----指定地址信息想要的长度以及返回实际的地址长度
返回值:成功则返回新获取的套接字的描述符----操作句柄;失败返回-1

5. 通过新获取的套接字操作句柄(accept返回的描述符)与指定客户端进行通信

接收数据:ssize_t recv(int sockfd, char *buf, int len, int flag);
返回值:成功返回实际读取的数据长度;
	   连接断开返回0;
	   读取失败返回-1
	   (类似管道读取数据:所有写端被关闭,则read返回0)
发送数据:ssize_t send(int sockfd, char *data, int len, int flag);
返回值:成功返回实际发送的数据长度;
	   失败返回-1
	   若连接断开触发异常

6. 关闭套接字

int close(int fd);

7. 客户端向服务端发送连接请求

int connect(int sockfd, int sockaddr *addr, socklen_t len);
sockfd:客户端套接字----若还未绑定地址,则操作系统会选择合适的源端地址进行绑定
addr:服务端地址信息----struct sockaddr_in;这个地址信息经过connect之后也会描述到socket中
len:地址信息长度

注意点

问题

  1. accept这个函数是一个阻塞函数:功能是获取新连接,如果当前没有新连接,则阻塞等待直到有新连接
  2. recv/send默认也是阻塞函数:接收缓冲区没有数据则recv阻塞 / 发送缓冲区满了则send阻塞
  • 因为当前TCP服务端流程是固定的(获取新连接、接收数据、发送数据),这几个功能都可能阻塞,导致流程无法推进

解决方案

  • 要防止流程阻塞,就要保证一个执行流只负责一个功能
  • 一个执行流只管获取新连接,当连接获取成功,然后创建新的执行流与客户端进行通信

多进程

  1. 父进程创建子进程,数据独有,各自有一份新的通信socket;子进程通过新的socket通信,父进程不需要通信,可以关闭
  2. 父进程要等待子进程退出,避免产生僵尸进程;为了父进程只负责获取新连接,因此对SIGCHLD信号自定义处理回调等待

多线程

  1. 主线程获取到新连接然后创建新线程与客户端进行通信,但是需要将套接字描述符传入线程执行函数中
  2. 但是传输这个描述符的时候,不能使用局部变量的地址传参(局部变量的空间在循环完毕就会释放),可以传描述符的值,也可以传入new对象
  3. 主线程中虽然不使用新创建的套接字,但是不能关闭,因为线程间共享资源,一个线程释放,另一个线程也无法使用

代码实现

封装Socket类

#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define BACKLOG 10
#define CHECK_RET(q) if((q) == false){return -1;}

class TcpSocket{
     
  public:
    TcpSocket():_sockfd(-1){
     }
    int GetFd(){
     
      return _sockfd;
    }
    void SetFd(int fd){
     
      _sockfd = fd;
    }
    // 创建套接字
    bool Socket(){
     
      _sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
      if(_sockfd < 0){
     
        perror("socket error!\n");
        return false;
      }
      return true;
    }

    void Addr(struct sockaddr_in *addr, const std::string &ip, uint16_t port){
     
      addr->sin_family = AF_INET;
      addr->sin_port = htons(port);
      inet_pton(AF_INET, ip.c_str(), &(addr->sin_addr.s_addr));
    }

    // 绑定地址信息
    bool Bind(const std::string &ip, const uint16_t port){
     
      // 1.定义IPv4地址结构
      struct sockaddr_in addr;
      Addr(&addr, ip, port);
      socklen_t len = sizeof(struct sockaddr_in);
      int ret = bind(_sockfd, (struct sockaddr*)&addr, len);
      if(ret < 0){
     
        perror("bind error!\n");
        return false;
      }
      return true;
    }
    // 服务端开始监听
    bool Listen(int backlog = BACKLOG){
     
      int ret = listen(_sockfd, backlog);
      if(ret < 0){
     
        perror("listen error!\n");
        return false;
      }
      return true;
    }
    // 客户端发起连接请求
    bool Connect(const std::string &ip, const uint16_t port){
     
      // 1. 定义IPv4地址结构,赋予服务端地址信息
      struct sockaddr_in addr;
      Addr(&addr, ip, port);
      // 2. 向服务端发起请求
      // 3. connect(客户端描述符, 服务端地址信息, 地址长度)
      socklen_t len = sizeof(struct sockaddr_in);
      int ret = connect(_sockfd, (struct sockaddr*)&addr, len);
      if(ret < 0){
     
        perror("connect error!\n");
        return false;
      }
      return true;
    }
    // 服务端获取新的连接
    bool Accept(TcpSocket *sock, std::string *ip = NULL, uint16_t *port = NULL){
     
      // accept(监听套接字, 对端地址信息, 地址信息长度) 返回新的描述符
      struct sockaddr_in addr;
      socklen_t len = sizeof(struct sockaddr_in);
      int clisock = accept(_sockfd, (struct sockaddr*)&addr, &len);
      if(clisock < 0){
     
        perror("accept error!\n");
        return false;
      }
      sock->_sockfd = clisock;
      if(ip != NULL){
     
        *ip = inet_ntoa(addr.sin_addr); // 网络字节序IP->字符串IP
      }
      if(port != NULL){
     
        *port = ntohs(addr.sin_port);
      }
      return true;
    }
    // 发送数据
    bool Send(const std::string &data){
     
      int ret = send(_sockfd, data.c_str(), data.size(), 0);
      if(ret < 0){
     
        perror("send error!\n");
        return false;
      }
      return true;
    }
    // 接收数据
    bool Recv(std::string *buf){
     
      char tmp[4096] = {
     0};
      int ret = recv(_sockfd, tmp, 4096, 0);
      if(ret < 0){
     
        perror("recv error!\n");
        return false;
      }else if(ret == 0){
     
        printf("connection break!\n");
        return false;
      }
      buf->assign(tmp, ret); // 从tmp中拷贝ret大小的数据到buf中
      return true;
    }
    // 关闭套接字
    void Close(){
     
      close(_sockfd);
      _sockfd = -1;
    }
  private:
    int _sockfd;
};

客户端

#include 
#include "tcpsocket.hpp"

int main(int argc, char *argv[])
{
     
  if(argc != 3){
     
    printf("em : ./tcp_cli 127.0.0.1 9000\n");
    return -1;
  }
  std::string ip = argv[1];
  uint16_t port = atoi(argv[2]);
  TcpSocket cli_sock;
  CHECK_RET(cli_sock.Socket());

  CHECK_RET(cli_sock.Connect(ip, port));

  while(1){
     
    printf("client say : ");
    fflush(stdout);
    std::string buf;
    std::cin >> buf;

    CHECK_RET(cli_sock.Send(buf));

    buf.clear();
    CHECK_RET(cli_sock.Recv(&buf));
    printf("server say : %s\n", buf.c_str());
  }
  cli_sock.Close();
  return 0;
}

服务端

  • 此处采用多线程实现
#include 
#include 
#include 
#include "tcpsocket.hpp"

void *thr_start(void *arg){
     
  long fd = (long)arg;
  TcpSocket cli_sock;
  cli_sock.SetFd(fd);
  while(1){
     
    std::string buf;
    if(cli_sock.Recv(&buf) == false){
     
      cli_sock.Close();
      pthread_exit(NULL);
      continue;
    }
    printf("client say : %s\n", &buf[0]);

    std::cout << "server say : ";
    fflush(stdout);
    buf.clear();
    std::cin >> buf;

    if(cli_sock.Send(buf) == false){
     
      cli_sock.Close();
      pthread_exit(NULL);
    }
  }
  cli_sock.Close();
  return NULL;
}

int main(int argc, char *argv[])
{
     
  if(argc != 3){
     
    printf("em : ./tcp_srv 127.0.0.1 9000\n");
    return -1;
  }
  std::string ip = argv[1];
  uint16_t port = atoi(argv[2]);

  TcpSocket lst_sock;
  CHECK_RET(lst_sock.Socket());
  CHECK_RET(lst_sock.Bind(ip, port));
  CHECK_RET(lst_sock.Listen());
  while(1){
     
    TcpSocket cli_sock;
    std::string cli_ip;
    uint16_t cli_port;

    bool ret = lst_sock.Accept(&cli_sock, &cli_ip, &cli_port);
    if(ret == false){
     
      continue;
    }

    printf("client : [%s : %d]", &cli_ip[0], cli_port);
    //---------------------------------------------
    pthread_t tid;
    // cli_sock是一个局部变量--循环完了这个资源就会被释放
    pthread_create(&tid, NULL, thr_start, (void*)cli_sock.GetFd());
    pthread_detach(tid); // 不关心线程返回值,分离线程,退出后自动释放资源
    // 主线程不能关闭cli_sock,因为多线程是共用描述符的
  }
  lst_sock.Close();
  return 0;
}

套接字与管道

  • 套接字与管道在某些方面是一致的

连接断开在发送端与接收端上的表现:

  1. 接收端:连接断开,则recv返回0;反之若recv返回0,表示的就是连接断开
  2. 发送端:连接断开,则send触发异常 SIGPIPE,导致进程退出

管道:

  1. 管道所有读端关闭,继续写入就会触发异常 SIGPIPE
  2. 管道所有写端关闭,继续读取就会返回0

TCP和UDP套接字的区别

UDP

客户端:socket / sendto / recvfrom / close
服务端:socket / bind / recvfrom / sendto / close

TCP

客户端:socket / connect / send / recv / close
服务端:socket / bind / listen / accept / recv / send / close

你可能感兴趣的:(内核,网络,socket,c++)