socket网络编程(二)封装socket函数

封装socket函数

socket 编程函数很多,细节也很多 — 封装起来操作更方便和安全

采用C++封装的意义主要有以下几方面。

1)把数据初始化的代码放在构造函数中;

2)把关闭socket等释放资源的代码放在析构函数中;

3)把socket定义为类的成员变量,类外部的代码根本看不到socket。

4)代码更简洁,更安全(析构函数自动调用关闭socket,释放资源)。

1、客户端socket封装
//TcpClient.h

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
 
class TcpClient
{
public:
  int m_sockfd;
 
  TcpClient();
 
  // 向服务器发起连接,serverip-服务端ip,port通信端口
  bool ConnectToServer(const char *serverip,const int port);
  // 向对端发送报文
  int  Send(const void *buf,const int buflen);
  // 接收对端的报文
  int  Recv(void *buf,const int buflen);
 
 ~TcpClient();
};
    
// 构造函数初始化m_sockfd    
TcpClient::TcpClient(){
  m_sockfd=0;  
}

// 析构函数关闭 m_sockfd
TcpClient::~TcpClient(){
  if (m_sockfd!=0) 
      close(m_sockfd);  
}
 
// 向服务器发起连接,serverip-服务端ip,port通信端口
bool TcpClient::ConnectToServer(const char *serverip,const int port)
{
  m_sockfd = socket(AF_INET,SOCK_STREAM,0); // 创建客户端的socket
 
  struct hostent* h; // ip地址信息的数据结构
  if ( (h=gethostbyname(serverip))==0 )
  { close(m_sockfd); m_sockfd=0; return false; }
 
  // 把服务器的地址和端口转换为数据结构
  struct sockaddr_in servaddr;
  memset(&servaddr,0,sizeof(servaddr));
  servaddr.sin_family = AF_INET;
  servaddr.sin_port = htons(port);
  memcpy(&servaddr.sin_addr,h->h_addr,h->h_length);
 
  // 向服务器发起连接请求
  if (connect(m_sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr))!=0)
  { close(m_sockfd); m_sockfd=0; return false; }
 
  return true;
}
 
int TcpClient::Send(const void *buf,const int buflen){
  return send(m_sockfd,buf,buflen,0);
}
 
int TcpClient::Recv(void *buf,const int buflen){
  return recv(m_sockfd,buf,buflen,0);
}

-------------------------------------------------------
//client.cpp
int main()
{
    TcpClient TcpClient;

    // 向服务器发起连接请求
    if (TcpClient.ConnectToServer("127.0.0.1",8081)==false){ 
        printf("TcpClient.ConnectToServer(\"118.89.50.198\",8081) failed,exit...\n"); 
        return -1; 
    }

    char strbuffer[1024];

    for (int ii=0;ii<5;ii++)
    {
        memset(strbuffer,0,sizeof(strbuffer));
        sprintf(strbuffer,"这是第%d个data,编号%03d。",ii+1,ii+1);
        if (TcpClient.Send(strbuffer,strlen(strbuffer))<=0) break;
        printf("发送:%s\n",strbuffer);

        memset(strbuffer,0,sizeof(strbuffer));
        if (TcpClient.Recv(strbuffer,sizeof(strbuffer))<=0) break;
        printf("接收:%s\n",strbuffer);
    }
}
2、服务端socket封装
//TcpServer.h
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
 
class TcpServer
{
    public:
    int m_listenfd;   // 服务端用于监听的socket
    int m_clientfd;   // 客户端连上来的socket

    TcpServer();

    bool InitServer(int port);  // 初始化服务端

    bool Accept();  // 等待客户端的连接

    // 向对端发送报文
    int  Send(const void *buf,const int buflen);
    // 接收对端的报文
    int  Recv(void *buf,const int buflen);

    ~TcpServer();
};
    
// 构造函数初始化socket
TcpServer::TcpServer(){ 
    m_listenfd=m_clientfd=0;
}

// 析构函数关闭监听和响应接受socket
TcpServer::~TcpServer(){
    if (m_listenfd!=0) close(m_listenfd);  
    if (m_clientfd!=0) close(m_clientfd);
}

// 初始化服务端的socket,port为通信端口
bool TcpServer::InitServer(int port){
    m_listenfd = socket(AF_INET,SOCK_STREAM,0);  // 创建服务端的socket

    // 把服务端用于通信的地址和端口绑定到socket上
    struct sockaddr_in servaddr;    // 服务端地址信息的数据结构
    memset(&servaddr,0,sizeof(servaddr));
    servaddr.sin_family = AF_INET;  // 协议族,在socket编程中只能是AF_INET
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);  // 本主机的任意ip地址
    servaddr.sin_port = htons(port);  // 绑定通信端口
    if (bind(m_listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)) != 0 )
    { close(m_listenfd); m_listenfd=0; return false; }

    // 把socket设置为监听模式
    if (listen(m_listenfd,5) != 0 ) 
    { close(m_listenfd); m_listenfd=0; return false; }

    return true;
}

bool TcpServer::Accept(){
    if ((m_clientfd=accept(m_listenfd,0,0)) <= 0) return false;
    return true;
}

int TcpServer::Send(const void *buf,const int buflen){
    return send(m_clientfd,buf,buflen,0);
}

int TcpServer::Recv(void *buf,const int buflen){
    return recv(m_clientfd,buf,buflen,0);
}

------------------------------------------------------------
//server.cpp
#include "TCPServer.cpp"
int main()
{
    TcpServer TcpServer;

    if (TcpServer.InitServer(8081)==false)
    { printf("TcpServer.InitServer(5051) failed,exit...\n"); return -1; }

    if (TcpServer.Accept() == false) { printf("TcpServer.Accept() failed,exit...\n"); return -1; }

    printf("客户端已连接。\n");

    char strbuffer[1024];

    while (1)
    {
        memset(strbuffer,0,sizeof(strbuffer));
        if (TcpServer.Recv(strbuffer,sizeof(strbuffer))<=0) break;
        printf("接收:%s\n",strbuffer);

        strcpy(strbuffer,"ok");
        if (TcpServer.Send(strbuffer,strlen(strbuffer))<=0) break;
        printf("发送:%s\n",strbuffer);
    }

    printf("客户端已断开连接。\n");
}
3、测试

makefile
在这里插入图片描述

查看状态,开启服务器端,可以看到TCP处在LISTEN状态;然后客户端发送数据之后断开连接,再次查看状态变成了TIME_WAIT。断开连接需要四次挥手,等待一段时间(2MSL),已防止最后的ACK分解丢失。

必须经过时间等待计时器设置的时间2MSL(最长报文寿命,为什么要等待这个时间 --> 如果客户端所发送的确认报文段没有到达服务器端,丢失了,服务器端无法收到确认;重传第三个报文段,也就是连接释放的报文段,客户端就可以在2MSL时间内收到重传的报文段,客户端就可以重传确认并且重启计时器;最后报文段没有发生丢失,就可以成功进入关闭状态)后,服务端才进入连接关闭状态。

在 Linux 系统里 2MSL 默认是 60 秒,那么一个 MSL 也就是 30 秒。Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒。最后一栏为60s之后查看的状态。

在这里插入图片描述

拓展:三次握手和四次挥手

1、基础知识

TCP 是面向连接的可靠的基于字节流的传输层通信协议。

面向连接:一定是「一对一」才能连接,不能像 UDP 协议可以一个主机同时向多个主机发送消息,也就是一对多是无法做到的;

可靠的:无论的网络链路中出现了怎样的链路变化,TCP 都可以保证一个报文一定能够到达接收端;

字节流:消息是「没有边界」的,所以无论我们消息有多大都可以进行传输。并且消息是「有序的」,当「前一个」消息没有收到的时候,即使它先收到了后面的字节,那么也不能扔给应用层去处理,同时对「重复」的报文会自动丢弃。— 通过TCP报头可以理解清楚。

建立一个 TCP 连接是需要客户端与服务器端达成上述三个信息的共识。

  • Socket:由 IP 地址和端口号组成
  • 序列号:用来解决乱序问题等
  • 窗口大小:用来做流量控制

TCP 四元组可以唯一的确定一个连接,四元组包括如下:源地址 源端口 目的地址 目的端口。

2、TCP 三次握手过程和状态变迁
TCP 三次握手
  • 一开始,客户端和服务端都处于 CLOSED 状态。先是服务端主动监听某个端口,处于 LISTEN 状态;
  • 客户端会随机初始化序号(client_isn),将此序号置于 TCP 首部的「序号」字段中,同时把 SYN 标志位置为 1 ,表示 SYN 报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于 SYN-SENT 状态。
  • 服务端收到客户端的 SYN 报文后,首先服务端也随机初始化自己的序号(server_isn),将此序号填入 TCP 首部的「序号」字段中,其次把 TCP 首部的「确认应答号」字段填入 client_isn + 1, 接着把 SYN 和 ACK 标志位置为 1。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于 SYN-RCVD 状态。
  • 客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部 ACK 标志位置为 1 ,其次「确认应答号」字段填入 server_isn + 1 ,最后把报文发送给服务端,这次报文可以携带客户到服务器的数据,之后客户端处于 ESTABLISHED 状态。
  • 服务器收到客户端的应答报文后,也进入 ESTABLISHED 状态。

从上面的过程可以发现第三次握手是可以携带数据的,前两次握手是不可以携带数据的。一旦完成三次握手,双方都处于 ESTABLISHED 状态,此时连接就已建立完成,客户端和服务端就可以相互发送数据了。

TCP 的连接状态查看,在 Linux 可以通过 netstat -napt 命令查看。
在这里插入图片描述

3、为什么需要三次握手?

三次握手才能保证双方具有接收和发送的能力。

具体来说,TCP 连接:用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括Socket、序列号和窗口大小称为连接。

  • 三次握手才可以防止旧的重复连接初始化造成混乱(主要原因)
  • 三次握手才可以同步双方的初始序列号(序列号能够保证数据包不重复、不丢弃和按序传输)
  • 三次握手才可以避免资源浪费

在网络拥堵情况下:

一个「旧 SYN 报文」比「最新的 SYN 」 报文早到达了服务端;
那么此时服务端就会回一个 SYN + ACK 报文给客户端;
客户端收到后可以根据自身的上下文,判断这是一个历史连接(序列号过期或超时),那么客户端就会发送 RST 报文给服务端,表示中止这一次连接。

如果是两次握手连接,就不能判断当前连接是否是历史连接,三次握手则可以在客户端(发送方)准备发送第三次报文时,客户端因有足够的上下文来判断当前连接是否是历史连接:

如果是历史连接(序列号过期或超时),则第三次握手发送的报文是 RST 报文,以此中止历史连接;
如果不是历史连接,则第三次发送的报文是 ACK 报文,通信双方就会成功建立连接;所以,TCP 使用三次握手建立连接的最主要原因是防止历史连接初始化了连接。

4、TCP 四次挥手过程和状态变迁
客户端主动关闭连接 —— TCP 四次挥手
  • 客户端打算关闭连接,此时会发送一个 TCP 首部 FIN 标志位被置为 1 的报文,也即 FIN 报文,之后客户端进入 FIN_WAIT_1 状态。

  • 服务端收到该报文后,就向客户端发送 ACK 应答报文,接着服务端进入 CLOSED_WAIT 状态。

  • 客户端收到服务端的 ACK 应答报文后,之后进入 FIN_WAIT_2 状态。

  • 等待服务端处理完数据后,也向客户端发送 FIN 报文,之后服务端进入 LAST_ACK 状态。

  • 客户端收到服务端的 FIN 报文后,回一个 ACK 应答报文,之后进入 TIME_WAIT 状态

  • 服务器收到了 ACK 应答报文后,就进入了 CLOSED 状态,至此服务端已经完成连接的关闭。

  • 客户端在经过 2MSL 一段时间后,自动进入 CLOSED 状态,至此客户端也完成连接的关闭。必须经过时间等待计时器设置的时间2MSL(**最长报文寿命,为什么要等待这个时间 **–> **如果客户端所发送的确认报文段没有到达服务器端,丢失了,服务器端无法收到确认;**重传第三个报文段,也就是连接释放的报文段,客户端就可以在2MSL时间内收到重传的报文段,客户端就可以重传确认并且重启计时器;最后报文段没有发生丢失,就可以成功进入关闭状态)后,A才进入连接关闭状态。

可以看到,每个方向都需要一个 FIN 和一个 ACK,因此通常被称为四次挥手。这里一点需要注意是:主动关闭连接的,才有 TIME_WAIT 状态。

再来回顾下四次挥手双方发 FIN 包的过程,就能理解为什么需要四次了。

关闭连接时,客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了但是还能接收数据。
服务器收到客户端的 FIN 报文时,先回一个 ACK 应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接。
从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的 ACK 和 FIN 一般都会分开发送,从而比三次握手导致多了一次。

参考1:C语言技术网

https://freecplus.net/44e059cca66042f0a1286eb188c51480.html

参考2:小林coding

https://blog.csdn.net/qq_34827674/article/details/105331617

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值