封装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 三次握手过程和状态变迁
- 一开始,客户端和服务端都处于
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 首部
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