网络socket编程: TCP通信流程以及代码实现

本文介绍了TCP协议的基本概念、通信流程,并详细讲解了socket接口的使用,包括创建套接字、绑定地址、连接请求、监听、接受连接、收发数据及关闭套接字等步骤。还提供了一个C++类`TcpSocket`来封装这些接口,同时展示了服务端和客户端的模拟实现。最后,讨论了如何处理多用户连续通信的问题。
摘要由CSDN通过智能技术生成

上次我们模拟实现了简易的UDP通信, 不知道大家有没有实现啊~
今天我们就来看看另一个非常非常非常重要的协议 — TCP协议



TCP通信流程

TCP — 传输控制协议

面向连接, 可靠传输, 面向字节流

tcp协议用于安全性要求大于实时性的场景 — 文件传输

下面给出TCP通信中客户端和服务端的各自的流程
在这里插入图片描述


socket接口

头文件<sys/socket.h>

1. 创建套接字

int socket(int domain, int type, int protocol);
domain: 地址域类型 -- AF_INET  (ipv4地址域)
type: 套接字类型 -- SOCK_STREAM 字节流服务 /  SOCK_DGRAM数据报服务
protocol: 协议类型,  0 默认类型/  IPPROTO_TCP/ IPPROTO_UDP
	字节流服务默认TCP协议, 数据报服务默认是UDP服务
返回值: 成功返回一个非负整数 -- 套接字描述符,  失败返回-1

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

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd: 创建套接字返回的操作句柄
addr:  struct socketaddr结构体,  不同的地址域有对应不同的地址结构
addrlen: 实际地址结构的长度
返回值: 成功返回0, 失败返回-1

3. 客户端发起连接请求

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

4. 服务端开始监听

int listen(int sockfd, int backlog);
backlog: 服务端最大并发连接数
(服务端同一时间能接收的客户端请求连接数, 并非总连接数)

5. 获取新建的客户端连接

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd: 套接字描述符 -- 监听套接字
addr:  新建socket连接对应的客户端地址
返回值: 成功返回新建客户端套接字的描述符--操作句柄, 失败返回-1

注意:

最早创建的并且listen的套接字, 只负责获取新建连接, 不与客户端通信
accept返回的套接字描述符(新建的套接字), 才是真正与客户端通信的套接字

6. 收发数据

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
返回值: 错误返回-1, 连接断开返回0, 成功返回实际接收的数据长度

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
这里的sockfd, 对于服务端来说一定是accept返回的描述符

7. 关闭套接字

int close(sockfd);

8. 地址转换接口

htons/htonl:  主机字节序到网络字节序的整数转换  short-16/int-32
ntohs/ntohl:  网络字节序到主机字节序的整数转换  short-16/int-32
点分十进制字符串IP地址到网络字节序IP地址的转换
   in_addr_t inet_addr(const char *cp); 
   
网络字节序IP地址到点分十进制字符串IP地址的转换
   char *inet_ntoa(struct in_addr in);
   
int inet_pton(int af, const char *src, void *dst);
af: 地址域类型 -- AF_INET,   src: 字符串IP地址,    
dst: 返回转换后的整数地址


const char *inet_ntop(int af, const void *src,  char *dst, socklen_t size);
af: 地址域类型,  src: 网络字节序整数IP,  
dst: 返回转换后的字符串,  size: dst空间长度

代码实现TCP通信

1. 封装socket接口

创建tcpsocket.hpp头文件, 进行接口的封装

#include <iostream>
#include <string>
#include <unistd.h>
#include <arpa/inet.h> //地址转换接口头文件
#include <netinet/in.h> // 地址结构类型定义头文件
#include <sys/socket.h> //套接字接口头文件

using namespace std;

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

class TcpSocket {
  public:
    TcpSocket() :_sockfd(-1) {}

    bool Socket() {
      _sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
      if(_sockfd < 0) {
        perror("socket error");
        return false;
      }
      return true;
    }
    
    //绑定地址信息
    bool Bind(const string& ip, const int port) {
      //定义地址结构
      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 = 5) {
     //int listen(int sockfd, backlog)
     int ret = listen(_sockfd, backlog);
     if (ret < 0) {
       perror("listen error");
       return false;
     }
     return true;
    }

    //客户端请求新连接
    bool Connect(const string& ip, int port) {
      //int connect(int sockfd, sockaddr* srvaddr, int addrlen)
      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(TcpSocket* sock, string* ip = nullptr, int* port = nullptr) {
      //int accept(int sockfd, sockaddr* addr, int* len);
      struct sockaddr_in cli_addr;
      socklen_t len = sizeof(struct sockaddr_in);
      int fd = accept(_sockfd, (struct sockaddr*)&cli_addr, &len);
      if(fd < 0) {
        perror("accept error");
        return false;
      }
      //传入的sock对象, 获取新建的连接套接字
      //fd是与cli_addr地址的客户端进行通信的, 外部通过sock与客户端进行通信
      sock->_sockfd = fd;
      if (ip != nullptr) {
        *ip = inet_ntoa(cli_addr.sin_addr);
      }
      if(port != nullptr) {
        *port = ntohs(cli_addr.sin_port);
      }
      return true;
    }

    //接收数据
    bool Recv(string* buf) {
      //ssize_t recv(int sockfd, char* buf, int len, len)
      //返回值: 错误返回-1, 连接断开返回0, 成功返回实际接收的数据长度
      
      char tmp[4096] = {0};
      int ret = recv(_sockfd, tmp, 4096, 0);
      if (ret < 0) {
        perror("recv error");
        return false;
      }
      else if (ret == 0) {
        printf("connect shutdown");
        return false;
      }
      buf->assign(tmp, ret);
      return true;
    }

    //发送数据
    bool Send(const string& data) {
      //ssize_t send(int sockfd, char* data, int len, int flag)
      size_t total_len = 0; //实际发送的数据长度
      while(total_len < data.size()) {
        int ret = send(_sockfd, data.c_str() + total_len, 
            data.size() - total_len, 0);
        if (ret < 0) {
          perror("send error");
          return false;
        }
        total_len += ret;
      }
      return true;
    }

    //关闭套接字
    bool Close () {
      if(_sockfd < 0) {
        close(_sockfd);
        _sockfd = -1;
      } 
      return true;
    }

  private:
    int _sockfd;
};

2. 模拟实现服务端

创建tcp_server.cpp文件, 进行服务端的实现

#include "tcpsocket.hpp"

int main() {
  //监听套接字
  TcpSocket lst_sock;
  
  //1.创建套接字
  CHECK_RET(lst_sock.Socket());
  
  //2.绑定地址信息
  CHECK_RET(lst_sock.Bind("0.0.0.0", 9000)); 
  
  //3.开始监听
  CHECK_RET(lst_sock.Listen());
  while(1) {
    //4.获取新连接
    TcpSocket con_sock;
    string cli_ip;
    int cli_port;
    int ret = lst_sock.Accept(&con_sock, &cli_ip, &cli_port);
    if (ret == false) {
      //获取失败就重新去获取
      continue;
    }
  
    //5.收发数据
    string buf;
    ret = con_sock.Recv(&buf);
    if (ret == false) {
      con_sock.Close();
      continue;
    }
    printf("[%s:%d] say:%s\n", cli_ip.c_str(), cli_port, buf.c_str());
    
    string data;
    cout << "server say: ";
    cin >> data;
    ret = con_sock.Send(data);
    if (ret == false) {
      con_sock.Close();
      continue;
    }
  }

  //6.关闭套接字
  lst_sock.Close();
  return 0;
}


3. 模拟实现客户端

创建tcp_client.cpp文件, 进行服务端的实现

#include "tcpsocket.hpp"

using namespace std;

int main() {
  TcpSocket cli_sock;

  //1.创建套接字
  CHECK_RET(cli_sock.Socket());
  //2.绑定地址信息(不推荐)
  //3.向服务端发起连接请求
  CHECK_RET(cli_sock.Connect("172.17.0.8", 9000));

  while(1) {
    //4.发送数据
    string data;
    cout << "client say: ";
    cin >> data;
    CHECK_RET(cli_sock.Send(data));
    //5.接收数据
    string buf;
    CHECK_RET(cli_sock.Recv(&buf));
    cout << "server say: " << buf << endl;
  }

  //6.关闭套接字
  cli_sock.Close();

  return 0;
}

到这里就实现完了~

编译后运行即可, 但是大家会发现tcp只能通信一次

原因: 服务端监听的时候, 获取新连接后收发一次数据, 就会再次进入下一次循环, 阻塞等待获取下一个新连接, 没有新连接就不会进行数据收发

那怎么解决呢?

把获取新连接放外面可以吗?

当然不行 ! ! 因为放外面的话, 只会获取一次新连接, 只能和一个用户进行连续通信

想要实现和多用户进行连续通信也是可以的, 这篇长度太长了, 就写到下一篇吧~

附上链接: 实现多执行流并发TCP服务器

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

殇&璃

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值