Socket基本操作的C++封装
目录
封装过程
接口类的实现(抽象类)
_public_socket.h
该头文件用于包含所有该系统平台socket所需要依赖的库。
- windows平台
#ifndef MY_TINY_STL__PUBLIC_SOCKET_H
#define MY_TINY_STL__PUBLIC_SOCKET_H
#include <winsock2.h>
#pragma comment (lib, "ws2_32.lib") //加载 ws2_32.dll
#endif //MY_TINY_STL__PUBLIC_SOCKET_H
- Linux平台
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
TCP_INTERFACE.h(作用于win平台)
由于该接口由服务器端和客户端继承,而两者同样的函数成员也就是这些了,设计客户端和服务器端时就只需要考虑各自的套接字以及其余操作的成员函数,也不需要管理DLL的开关。 还有一个erro_die()成员函数用于阻断错误并打印对应情况。
//
// Created by Alone on 2021/8/17.
//
#ifndef MY_TINY_STL_TCP_INTERFACE_H
#define MY_TINY_STL_TCP_INTERFACE_H
#include <cstdio>
#include "_public_socket.h"
class TCP_INTERFACE {
public:
TCP_INTERFACE() {
//初始化 DLL
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
}
//返回值小于等于0时发生错误
virtual int Send(SOCKET clnt, const void *buf, const int buflen) = 0;
virtual int Recv(SOCKET clnt, void *buf, const int buflen) = 0;
//closesocket返回值不为0则发生错误
virtual void Close(SOCKET clnt) = 0;
virtual void error_die(const char *str) = 0;
~TCP_INTERFACE() {
WSACleanup();
}
};
#endif //MY_TINY_STL_TCP_INTERFACE_H
服务器端封装
这次修改了下逻辑,还是用accept返回一个套接字进行发送和接收操作比较好。类的底层没有再保留用于和某个客户端通信的套接字了,自己控制各个客户端套接字的关闭和使用。
TCP_SOCKET_SERVER.h
//
// Created by Alone on 2021/8/16.
//
#ifndef MY_TINY_STL_TCP_SOCKET_SERVER_H
#define MY_TINY_STL_TCP_SOCKET_SERVER_H
#include "TCP_INTERFACE.h"
class TCP_SOCKET_SERVER : public TCP_INTERFACE {
public:
TCP_SOCKET_SERVER();
~TCP_SOCKET_SERVER();
void Bind(int port);
void Listen();
SOCKET Accept();
int Send(SOCKET clnt, const void *buf, const int buflen);
int Recv(SOCKET clnt, void *buf, const int buflen);
void Close(SOCKET clnt);
void error_die(const char *str);
private:
SOCKET servSock;
sockaddr_in sockAddr;
};
#endif //MY_TINY_STL_TCP_SOCKET_SERVER_H
TCP_SOCKET_SERVER.cpp
//
// Created by Alone on 2021/8/16.
//
#include "TCP_SOCKET_SERVER.h"
//初始化操作
TCP_SOCKET_SERVER::TCP_SOCKET_SERVER() : servSock(0) {
memset(&sockAddr, 0, sizeof(sockAddr)); //每个字节都用0填充
}
//绑定操作
void TCP_SOCKET_SERVER::Bind(int port) {
servSock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
sockAddr.sin_family = PF_INET; //使用IPv4地址
sockAddr.sin_addr.s_addr = htonl(INADDR_ANY); //具体的IP地址
sockAddr.sin_port = htons(port); //端口
if (bind(servSock, (SOCKADDR *) &sockAddr, sizeof(SOCKADDR)) != 0) {
error_die("bind");
}
}
//置于监听状态
void TCP_SOCKET_SERVER::Listen() {
if (servSock == 0)
error_die("listen");
if (listen(servSock, SOMAXCONN) != 0) {
error_die("listen");
}
}
//利用套接字的监听串口,接收客户端的请求,建立新的套接字进行存储信息
SOCKET TCP_SOCKET_SERVER::Accept() {
SOCKADDR t;
int nSize = sizeof(SOCKADDR);
//后面两个参数为可选
SOCKET clnt = accept(servSock, &t, &nSize);
if (clnt <= 0)error_die("accept");
return clnt;
}
//返回的是发送到缓冲区的字节长度
int TCP_SOCKET_SERVER::Send(SOCKET clnt, const void *buf, const int buflen) {
return send(clnt, (const char *) buf, buflen, 0);
}
//返回已经接收的字节长度
int TCP_SOCKET_SERVER::Recv(SOCKET clnt, void *buf, const int buflen) {
return recv(clnt, (char *) buf, buflen, 0);
}
//析构函数关闭socket
TCP_SOCKET_SERVER::~TCP_SOCKET_SERVER() {
if (servSock != 0)closesocket(servSock);
}
void TCP_SOCKET_SERVER::Close(SOCKET clnt) {
if (closesocket(clnt) != 0) {
error_die("closesocket");
}
}
void TCP_SOCKET_SERVER::error_die(const char *str) {
printf("[hint]%s failed:%d", str, WSAGetLastError());
exit(-1);
}
客户端的封装
TCP_SOCKET_CLIENT.h
增加了利用域名查询ip地址的成员函数gethostbyname(),挺好玩的!此次增加了erro_die函数,且发送和接收都操作套接字。在类的内部还是保留了套接字的备份,用于忘记关闭套接字时,析构函数进行关闭。
//
// Created by Alone on 2021/8/18.
//
#ifndef MY_TINY_STL_TCP_SOCKET_CLIENT_H
#define MY_TINY_STL_TCP_SOCKET_CLIENT_H
#include "TCP_INTERFACE.h"
#include <iostream>
class TCP_SOCKET_CLIENT : public TCP_INTERFACE {
public:
TCP_SOCKET_CLIENT();
~TCP_SOCKET_CLIENT();
SOCKET Connect(const char *IPAdrr, u_short port);
//用于利用URL(域名)查询IP地址
void Gethostbyname(const char *URL);
//接口必须实现的函数
int Send(SOCKET clnt,const void *buf, const int bufSize);
int Recv(SOCKET clnt,void *buf, const int bufSize);
void Close(SOCKET clnt);
void error_die(const char *str);
private:
//由于一般客户端只需要一个套接字实现连接,然后还需要一个socketadrr_in用于连接内容的赋值
SOCKET clntSock;
sockaddr_in sockAddr;
};
#endif //MY_TINY_STL_TCP_SOCKET_CLIENT_H
TCP_SOCKET_CLIENT.cpp
//
// Created by Alone on 2021/8/17.
//
#include "TCP_SOCKET_CLIENT.h"
//初始化
TCP_SOCKET_CLIENT::TCP_SOCKET_CLIENT() : clntSock(0) {}
//关闭套接字操作
void TCP_SOCKET_CLIENT::Close(SOCKET clnt) {
if (closesocket(clnt) != 0)
error_die("close");
clntSock = 0;
}
//连接服务器操作
SOCKET TCP_SOCKET_CLIENT::Connect(const char *IPAdrr, u_short port) {
memset(&sockAddr, 0, sizeof sockAddr);
clntSock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
sockAddr.sin_family = PF_INET;
sockAddr.sin_addr.s_addr = inet_addr(IPAdrr);
sockAddr.sin_port = htons(port);
if (connect(clntSock, (SOCKADDR *) &sockAddr, sizeof(sockAddr)) != 0) {
error_die("connect");
}
return clntSock;
}
//发送信息操作
int TCP_SOCKET_CLIENT::Send(SOCKET clnt,const void *buf, const int bufSize) {
return send(clnt, (const char *) buf, bufSize, 0);
}
//接收信息操作
int TCP_SOCKET_CLIENT::Recv(SOCKET clnt,void *buf, const int bufSize) {
return recv(clnt, (char *) buf, bufSize, 0);
}
//根据域名获取ip地址等信息
void TCP_SOCKET_CLIENT::Gethostbyname(const char *URL) {
hostent *host = gethostbyname(URL);
if (!host) {
std::cout << "Get IP address error!\n";
return;
}
//打印本命
std::cout << URL << std::endl;
//别名
for (int i = 0; host->h_aliases[i]; i++) {
printf("Aliases %d: %s\n", i + 1, host->h_aliases[i]);
}
//地址类型
printf("Address type: %s\n", (host->h_addrtype == AF_INET) ? "AF_INET" : "AF_INET6");
//IP地址,其中inet_ntoa()函数是将网络字节序转为本地的字节序,方便打印看懂
for (int i = 0; host->h_addr_list[i]; i++) {
printf("IP addr %d: %s\n", i + 1, inet_ntoa(*(struct in_addr *) host->h_addr_list[i]));
}
}
//析构时需要确保所有东西已经关闭
TCP_SOCKET_CLIENT::~TCP_SOCKET_CLIENT() {
if (clntSock != 0)
closesocket(clntSock);
}
void TCP_SOCKET_CLIENT::error_die(const char *str) {
printf("[hint]%s failed:%d", str, WSAGetLastError());
exit(-1);
}
实例讲解
实例一:回声程序通信
服务器回声程序
绑定本地1234端口,进入监听状态等待请求,如果通信对象关闭了通信,也不慌,重新goto到等待请求得到新的通信套接字
#include <iostream>
#include "TCP_SOCKET_SERVER.h"
#define BUF_SIZE 1000
using namespace std;
int main() {
TCP_SOCKET_SERVER a;
a.Bind(1234);
a.Listen();
restart:
SOCKET clnt = a.Accept();
while (1) {
char *x = new char[BUF_SIZE];
memset(x, 0, BUF_SIZE);
int size = a.Recv(clnt,x, BUF_SIZE);
if (size <= 0)
break;
if (a.Send(clnt,x, size) <= 0)
break;
}
a.Close(clnt);
cout << "connect is over.Waiting for a new connection!\n";
goto restart;
}
客户端通信程序
为保持持续通信,一旦客户端拒绝了请求,那么弹出循环重新连接,并设置连接超时操作。
#include "TCP_SOCKET_CLIENT.h"
#define BUF_SIZE 100
int main(){
TCP_SOCKET_CLIENT t;
const char* to = "127.0.0.1";
restart:
SOCKET clnt = t.Connect(to,1234);
while(1){
std::cout<<"\nInput your message:\n";
char buf[BUF_SIZE] = {0};
std::cin.getline(buf,99);
int size = t.Send(clnt,buf,BUF_SIZE);
if(size<=0)
break;
memset(buf,0,sizeof buf);
if(t.Recv(clnt,buf,size)<=0)
break;
printf("received from %s is:\n",to);
std::cout<<buf;
}
t.Close(clnt);
std::cout<<"The Server is disconnected,and socket has been cleaned up,socket connection has been re-established\n";
goto restart;
return 0;
}
回声效果
客户端收到的结果
服务器端一直运行着,只要不关闭,但每次只能和一个客户端进行通信,通信完后重新等待连接。
实例二:文件操作,传送图片(掌握重复传包)
分析待传图片
看看这百万大小的字节,一次肯定是传不完的,所以我们需要发送端不断的续传,直到传送完毕。
发送端程序
#include "TCP_SOCKET_CLIENT.h"
#include <fstream>
int main(){
TCP_SOCKET_CLIENT t;
const char* to = "127.0.0.1";
restart:
SOCKET clnt = t.Connect(to,1234);
//图片写入buf(这几百万字节大小,得亏是new动态分配
std::ifstream img("D:/DesktopBackground/L-69.png",std::ios::in|std::ios::binary);
//设置文件指针用于求文件内容长度
img.seekg(0,std::ios::end);
int len = img.tellg();
img.seekg(0,std::ios::beg);
if(len>0){printf("read OK\n");}
else {printf("file is empty!");return 0;}
//填补buf
char * buf = new char[len];
img.read(buf,len);
//发送数据到服务器,一次肯定发送不完,所以多次
int sum = 0;
while(sum<len){
int sendlen = t.Send(clnt,buf,len);
if(sendlen<=0){
printf("Send Erro!");
return 0;
}
sum += sendlen;
}
t.Close(clnt);
printf("Send OK!");
return 0;
}
接收端程序
#include <iostream>
#include "TCP_SOCKET_SERVER.h"
#include <fstream>
#define BUF_SIZE 100
using namespace std;
int main() {
TCP_SOCKET_SERVER a;
a.Bind(1234);
a.Listen();
//等待连接,连接成功便可建立通讯
SOCKET clnt = a.Accept();
//创建文件用于写入图片数据
ofstream t("test.png",ios::binary|ios::out);
//由于要接收的图片文件较大,需要分多次包进行传输数据,所以需要不断循环接收
while(1){
char buf[BUF_SIZE];
int sz = a.Recv(clnt,buf,BUF_SIZE);
//直到发送端发送数据完毕断开连接后,便可判断为接收完毕
if(sz<=0){
cout<<"Finish !";
return 0;
}
//每次调整文件指针位置到最后续写
t.seekp(0,ios::end);
t.write(buf,sz);
}
a.Close(clnt);
}
接收结果
一模一样毫无偏差
实例三:Web通信(浏览器访问服务器)
Web服务器程序
我这个web服务器也算是及其简单了。。并没有对客户端的http请求进行解析然后发送对应的文件给客户端,而是单纯的我客户端想怎么发就怎么发。。另外这个程序虽然是对图片进行了判断,但并未写出对应的图片发送程序(二进制文件读写是不一样的),所以实际只能发送文本文件,如html代码。所以后面看到的课程表都无法显示图片的原因是客户端程序压根就没在乎过客户端的请求🤣
#include <iostream>
#include <fstream>
#include "TCP_SOCKET_SERVER.h"
void sendfileToWeb(SOCKET clnt, TCP_SOCKET_SERVER &a, const char *filename);
int main() {
TCP_SOCKET_SERVER a;
a.Bind(8086);
a.Listen();
SOCKET clnt = a.Accept();
while (1) {
sendfileToWeb(clnt, a, "D:/Html/schedule/schedule.html");
a.Close(clnt);
clnt = a.Accept();
}
}
//反馈请求,发送文件代码或者图片等二进制信息。
void sendfileToWeb(SOCKET clnt, TCP_SOCKET_SERVER &a, const char *filename) {
//写入返回头信息:包括状态和内容类型
char *type = nullptr;
if (strstr(filename, ".html"))
type = "text/html";
else if (strstr(filename, ".jpg"))
type = "image/jpg";
else if (strstr(filename, ".png"))
type = "image/png";
char x[100] = {0};
sprintf(x, "HTTP/1.1 200 ok\r\nContent-Type: %s\r\n\r\n", type);
//发送返回头信息,每次发送间隔需要一定时间,否则浏览器可能接收没这么快
int sz1 = a.Send(clnt, x, strlen(x));
if (sz1 <= 0)return;
//发送文件内容到客户端
std::ifstream ss(filename, std::ios::in);
char html[1024] = {0};
while (ss.getline(html, 1024)) {
int szz = a.Send(clnt, html, strlen(html));
if (szz <= 0)
return;
Sleep(1);
}
ss.close();
}
接收结果
总结
收获:
- 了解到网络通讯过程到底是怎么样的。
- 了解到底层socket通信是如何进行的。
- 封装了socket操作,增强了代码的复用性。
- 对基本的http请求过程有所了解: 基本上就是浏览器(客户端)对相应的IP地址发起请求,其对应的服务器返回给你这个网页的主页,然后根据你鼠标的点击,又会触发http请求,其对应的服务器对你的请求进行解析,得出你想要的文件,然后发送给你,循环往复一直如此。。。