前天老师循序渐进的给了这个任务,具体任务内容如下,经过一天半的搜索拼凑调试,在各路csdn博主清晰明了的优秀文章的帮助下,最后总算是实现了,简单总结下。
任务内容:
1.C++实现socket通信;2.socket传输数据要封装成json格式;3.json传输图片
具体实现:
一、c++实现socket通信
1.1.1服务端步骤:
1、加载套接字库,创建套接字(WSAStartup()/socket());
2、绑定套接字到一个IP地址和一个端口上(bind());
3、将套接字设置为监听模式等待连接请求(listen());
4、请求到来后,接受连接请求,返回一个新的对应于此次连接的套接字(accept());
5、用返回的套接字和客户端进行通信(send()/recv());
6、返回,等待另一个连接请求;
7、关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup());
1.1.2方法详解:
1)加载Winsock库
/**
int WSAStartup(__in WORD wVersionRequested,__out LPWSADATA lpWSAData);
此函数在应用程序中初始化winsockDLL,只有此函数调用成功后,应用程序才可以调用Windows SocketsDLL中的其他API函数,否则后面的任何函数都将调用失败
wVersionRequested -- 调用程序使用windows socket的最高版本。 高字节指定小的版本号,低字节指定高的版本号。
lpWSAData -- 指向WSADATA数据结构体指针,接收Windows Socket的实现细节。
返回值
如果成功,WSAStartup函数返下面列表显示的回0。否则,返之一回错误码。
*/
WORD sockVersion = MAKEWORD(2, 2);
WSADATA wsaData;
if (WSAStartup(sockVersion, &wsaData) != 0)
{
return 0;
}
2)创建套接字
/**
int socket (int domain, int type, int protocol)
初始化创建socket对象,成功时,返回非负数的socket描述符;失败是返回-1。
domain -- 指明使用的协议族,协议族决定了socket的地址类型,在通信中必须采用对应的地址,AF_INET表示ipv4地址(32位的)与端口号(16位的)的组合
type -- 指明socket类型,SOCK_STREAM表示TCP类型,保证数据顺序及可靠性;
protocol -- 通常赋值"0",由系统自动选择。
*/
SOCKET slisten = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (slisten == INVALID_SOCKET)
{
printf("socket error !");
return 0;
}
3)配置监听地址和端口并绑定
/**
int bind(int sockfd, const struct sockaddr* myaddr, socklen_t addrlen)
返回值:0 -- 成功,-1 -- 出错
sockfd -- socket()函数返回的描述符
myaddr -- 指明要绑定的本地IP和端口号,使用网络字节序
addrlen -- 常被设置为sizeof(struct sockaddr)
*/
sockaddr_in sin;
sin.sin_family = AF_INET;// IP地址家族
sin.sin_port = htons(8888);// 填写端口,1~1024是保留端口号,可以使用大于1024中任何一个没有被占用的端口号
sin.sin_addr.S_un.S_addr = INADDR_ANY;//填写ip,INADDR_ANY表示使用本机ip地址
if (bind(slisten, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR)
{
printf("bind error !");
}
关于htons:将主机的无符号短整形数转换成网络字节顺序,返回一个网络字节顺序的值
关于sockaddr_in:它是一个结构体,成员中还有一个结构体in_addr, 其中sin_addr 就是要填写的IP地址,它们的具体定义如下
struct sockaddr_in {
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
typedef struct in_addr {
union {
struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b;
struct { u_short s_w1,s_w2; } S_un_w;
u_long S_addr;
} S_un;
} in_addr;
4)开始监听
/**
int listen(int sockfd, int backlog)
listen()函数仅被TCP类型的服务器程序调用,实现监听服务。成功时返回0,错误时返回-1。
ockfd -- socket()函数返回的描述符;
backlog -- 指定内核为此套接字维护的最大连接个数即此套接字排队的最大连接个数,包括“未完成连接队列--未完成3次握手”、“已完成连接队列--已完成3次握手,建立连接”。大多数系统缺省值为20。
*/
if (listen(slisten, 5) == SOCKET_ERROR)
{
printf("listen error !");
return 0;
}
5)接受连接请求
/**
int accept (int sockfd, struct sockaddr *addr, socklen_t *addrlen)
成功时返回套接字描述符,错误时返回-1。
sockfd -- socket()函数返回的描述符;
addr -- 输出一个的sockaddr_in变量地址,该变量用来存放发起连接请求的客户端的协议地址;
addrten -- 作为输入时指明缓冲器的长度,作为输出时指明addr的实际长度。
*/
SOCKET sClient;
sockaddr_in remoteAddr;
int nAddrlen = sizeof(remoteAddr);
sClient = accept(slisten, (SOCKADDR *)&remoteAddr, &nAddrlen);
if (sClient == INVALID_SOCKET)
{
printf("accept error !");
continue;
}
printf("接受到一个连接:%s \r\n", inet_ntoa(remoteAddr.sin_addr));
6)接收数据(5、6都是在循环中的)
/**
int recv(int sockfd, void *buf, int len, unsigned int flags)
从接收缓冲区拷贝数据,TCP类型的数据接收。有数据则返回拷贝的数据大小,否则返回错误-1
sockefd -- 接收端套接字描述符;
buf -- 接收缓冲区的基地址;
len -- 以字节计算的接收缓冲区长度;
flags -- 一般情况下置为0。
*/
char revData[DATA_SIZE]; //DATA_SIZE是宏定义了一个大小
int ret = recv(sClient, revData, DATA_SIZE, 0);
if (ret > 0)
{
//处理接收到的数据
}
7)关闭socket
closesocket(sClient);
closesocket(slisten);
8)释放Winsock库
WSACleanup();
1.2.1客户端编程步骤:
1、加载套接字库,创建套接字(WSAStartup()/socket());
2、向服务器发出连接请求(connect());
3、和服务器进行通信(send()/recv());
4、关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup());
1.2.2方法详解:
1)加载Winsock库,创建套接字(同服务端)
2)请求连接
/**
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen)
TCP类型客户端调用,用来与服务器建立一个TCP连接,实际是发起3次握手过程,连接成功返回0,连接失败返回1
sockfd -- 本地客户端额socket描述符;
serv_addr -- 服务器协议地址;
addrlen -- 地址缓冲区的长度。
*/
sockaddr_in serAddr;
serAddr.sin_family = AF_INET;
serAddr.sin_port = htons(8888);
serAddr.sin_addr.S_un.S_addr = inet_addr("192.168.4.110");//inet_addr()作用也是IP地址格式转换
if (connect(sclient, (sockaddr *)&serAddr, sizeof(serAddr)) == SOCKET_ERROR)
{ //连接失败
printf("connect error !");
closesocket(sclient);
return 0;
}
3)发送数据
/**
int send(int sockfd, const void *msg, int len, int flags)
每个TCP套接口都有一个发送缓冲区,它的大小可以用SO_SNDBUF这个选项来改变。调用send函数的过程,实际是内核将用户数据拷贝至TCP套接口的发送缓冲区的过程:若len大于发送缓冲区大小,则返回-1;否则,查看缓冲区剩余空间是否容纳得下要发送的len长度,若不够,则拷贝一部分,并返回拷贝长度;若缓冲区满,则等待发送,有剩余空间后拷贝至缓冲区;若在拷贝过程出现错误,则返回-1。关于错误的原因,查看errno的值。
sockfd -- 发送端套接字描述符(非监听描述符)。
msg -- 待发送数据的缓冲区。
len -- 待发送数据的字节长度。
flags -- 一般情况下置为0。
*/
send(sclient, sendData, strlen(sendData), 0);
4)释放Winsock库
二、关于json
json之前写php的时候使用过,php里也直接有方法直接就可以封装成这种格式,但是C++就不一样了,刚接到这个任务时搜了很多,有用jsoncpp的,下载下来配置了一会还是没有成功,老师给的是一个gitHub上面的开源项目,查了一下使用方法,简直太方便了
GitHub开源项的地址:https://github.com/nlohmann/json
使用方法:
1)github上下载json.hpp
2)把下载的json.hpp文件放到你要是用的项目的头文件夹下
3)在代码中引入头文件以及json作用域
#include "json.hpp"
using json = nlohmann::json;
这样就可以用了,关于使用的话里面封装了很多,这次用到了一些,例如
1)创建json对象并以根据键直接生成键值对
json j;
//添加一个存储为double的数字
j["pi"] = 3.141;
// 添加一个布尔值
j["happy"] = true;
//添加一个存储为std :: string的字符串
j["name"] = "Niels";
cout<
2)转换为string
//显式转换为string
std::string s = j.dump();
//socket 里面send()方法发送数据必须是const char*,所以要专程string,再转成const char*
const char * sendData;
sendData = s.c_str(); //c_str():生成一个const char*指针,指向以空字符终止的数组
3)将数据流转化为json对象,使用json::parse()函数
//服务端接收到数据后这样做,revData是接收数据的buffer
json o = json::parse(revData);
4)使用迭代器输出
for (json::iterator it = o.begin(); it != o.end(); ++it) {
std::cout << it.key() << " : " << it.value() << "\n";
}
三、json传输图片
这个的总体思路是客户端先以二进制形式读取图片文件,因为json不能传输二进制所以使用base64编码转换成string,封装到json中,发送给服务端,服务端接收后先json解析,然后base64解码,然后再转二进制写入新建的.jpg文件。这个思路应该是没什么问题的,但是C++不熟啊,所以各种坎坷
1)二进制打开图片文件
/**
FILE *fopen(const char *filename, const char *mode)
返回一个文件指针
filename -- 这是 C 字符串,包含了要打开的文件名称。
mode -- 这是 C 字符串,包含了文件访问模式,rb表示打开一个用于二进制读取的文件
*/
//定义文件指针
FILE *fIn1;
int i=1;//因为我们做了一个循环,所以这里定义了一个int
std::string num = std::to_string(i);//因为要拼接到路径string里,所以要做个类型转换
std::string chFileIn1 = "D:\\" + num + ".jpg";
char chFileIn3[100]
strcpy(chFileIn3, chFileIn1.c_str());
fIn1 = fopen(chFileIn3, "rb");
if (fIn1 == NULL )
{
printf("打开读取文件失败");
return 0;
}
2)读取图片文件
/**
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
从给定流 stream 读取数据到 ptr 所指向的数组中,返回整型表示成功读取的元素总数
ptr -- 这是指向带有最小尺寸 size*nmemb 字节的内存块的指针。
size -- 这是要读取的每个元素的大小,以字节为单位。
nmemb -- 这是元素的个数,每个元素的大小为 size 字节。
stream -- 这是指向 FILE 对象的指针,该 FILE 对象指定了一个输入流。
*/
#define DATA_SIZE 3888888 //这个数根据需要写
int nRead1;
char chBuf1[DATA_SIZE];
nRead1 = fread(chBuf1, sizeof(char), DATA_SIZE, fIn1);
3)读取的内容做base64编码,这里找一个头文件引进来直接用了(具体见【参考】6)
/**
base64_encode(const char * bytes_to_encode, unsigned int in_len)
这就是引入的base64.h里面的的方法,返回string
bytes_to_encode -- 你需要编码的元素集(不懂的话把这几步连起来看下我传的参数,应该就理解了)
in_len -- 元素的个数
*/
//base64编码
string imgBase64_1 = base64_encode(chBuf1, nRead1);
4)封装进json,具体按照前面json里面系的去做就可以
json data1;
data1["imgA"] = imgBase64_1;
=================好了客户端发送了,服务端要接收解析并保存======================
5)json解析base64解码保存图片,之前搜到的base64.h中也有解码的方法,但是解码后返回string,想把它转成二进制写入文件的,但是搜到c++二进制就是unsigned char* ,然后又尝试写文件,试了很多遗憾没有成功,还是对c++不通吧,放弃了。所以又搜到了一个并且用了一个WritePhotoFile()方法(具体见【参考4】我还没有仔细读这段代码,所以读了再补充吧)
/**
bool WritePhotoFile(std::basic_string strFileName, std::string &strData)
这个方法里面会做base64解码,并且保存成jpg文件
strFileName -- 文件路径及文件名
strData -- 需要解码的数据
*/
//这个就是写在【二、关于json的4)循环里面】
int i=0;//还是做了多个,所以定义一个用来计数
if (it.key() == "imgA")
{
std::string num = std::to_string(i++);
std::string strFileName = "D:\\"+ num +".jpg"; //文件路径及文件名,例如D://1.jpg
std::string val = it.value();
WritePhotoFile(strFileName, val);
}
【参考】
1.socket接口详解
2.json for modern c++的使用
3.windows环境下用c++实现socket编程
4.C++读写图片数据转成Base64格式的一种方法
5.C/C++ 图像二进制存储与读取
6.C++ 实现图片base64编解码
(参考的这些文章都写得非常详细,感谢)