原生Socket TCP实现HTTP文件上传
Posted FreeFly辉
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了原生Socket TCP实现HTTP文件上传相关的知识,希望对你有一定的参考价值。
前言
java开发一枚,想用qt写一个简单的桌面应用(qt可以直接打包成exe,java却要jre环境)。苦于不熟悉c++,因此考虑用原生socket实现调用http接口。此文针对客户端进行讲解实现。
环境
服务端:springboot 提供的文件上传接口客户端:c++原生TCP协议 socket
此文主要针对http协议进行解析,非同时了解java c++的小伙伴注意看解析,自己用对应语言实现起来也很简单。
服务端
服务端不想说太多讲解,非socket实现,几分钟搭建的一个简单springboot,并提供了一个文件上传接口。(初学者可用tomcat提供的servlet,srpringmvc提供一个接口)服务端代码(非常经典的boot controller,因为demo实现直接写在了controller):
@RestController
@RequestMapping("image")
public class ImageController
@PostMapping("uploadImg")
public void uploadImg(MultipartFile file)
FileOutputStream fos = null;
try
InputStream is = file.getInputStream();
File fileNew = new File("mypic/upload.jpeg");
fos = new FileOutputStream(fileNew);
byte[] bytes = new byte[1024];
int length;
while ((length = is.read(bytes)) != -1)
fos.write(bytes,0,length);
fos.flush();
catch (Exception e)
e.printStackTrace();
finally
try
fos.close();
catch (IOException e)
客户端
主要针对客户端,注意看注释
//在对象构造方法中进行初始化,c++必须步骤,其他语言也有类似步骤
SocketUtil::SocketUtil()
WORD wVersion = MAKEWORD(2, 2);
WSADATA wsadata;
if (WSAStartup(wVersion, &wsadata) != 0)
throw "初始化socket连接异常";
add.sin_family = AF_INET;//协议簇
add.sin_addr.S_un.S_addr = ::inet_addr("127.0.0.1");//主动连接该ip地址
add.sin_port = htons(8899);//端口
//初始化完成,进行对应socket连接,各类语言也都有对象步骤,无需深究
void SocketUtil::connectServer()
client = socket(AF_INET, SOCK_STREAM, 0);
int len = sizeof(sockaddr_in);
//创建连接
int i = ::connect(client, (sockaddr*)&add, len);
if (SOCKET_ERROR==i)
throw "连接失败:"+to_string(i);
以上代码为初始化socket,以及与服务端创建连接代码,任何语言都有类似步骤,无需深究。下面为具体的重要代码:
void SocketUtil::executePostFile(string url,string file)
connectServer(); //调用初始化中的socket连接方法
ifstream afile; //c++打开文件的对象
afile.open(file, ios::in);//打开一个文件
afile.seekg(0, afile.end);//跳到文件末尾
int length = afile.tellg();//获取当前光标所在,字节下标,(配合上一步的跳到文件末尾,从而获取文件大小)
afile.seekg(0, afile.beg);//跳到文件开始,获取完长度,将光标重置
char * buffer = new char[length];//新建字节数组,用于保存文件数据
string BOUNDARY = "----WebKitFormBoundaryeQ0uB5bSgAI8zsNS";//http中表单分割符,可随便自定义,此处为谷歌浏览器抓包复制下来用的,非常重要
string header = "POST "+url+" HTTP/1.1\\r\\n";//利用string类型,进行http请求头的构建,此处构建了 请求方法,请求路径(注意此处的 url指的是http中端口后的匹配路径),协议类型
//----------------常规请求头构建,可根据需要自行增减,例如可以携带cookie之类的
header+="Host: localhost:8899\\r\\n";
header+="Connection: keep-alive\\r\\n";
header+="Accept: */*\\r\\n";
header+="Access-Control-Request-Method: POST\\r\\n";
header+="Access-Control-Request-Headers: content-type,username\\r\\n";
header+="User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (Khtml, like Gecko) Chrome/92.0.4515.107 Safari/537.36\\r\\n";
//-------------常规请求头构建结束
/**请求头中 请求体长度构建,非常重要,指定了接下来请求体表单所有字节数长度,单位byte
* 其中length是文件长度,183是除文件外,经过计算出来的 BOUNDARY分隔符,\\r\\n等所有字节长度。
*/
header+="Content-Length: "+to_string(length+183)+"\\r\\n";//183 是后续请求体中 分隔符BOUNDARY + content +/r/n " 等的所有字节个数。
//请求体类型 multipart/form-data,表示请求体是一个表单,包含类似 key-value形式的多个参数,http协议规定 以\\r\\n作为分割符,而连续的 \\r\\n\\r\\n,表示请求头发送完毕
header+="Content-Type: multipart/form-data; boundary="+BOUNDARY+"\\r\\n\\r\\n";
//利用socket,发送构建的请求头
int ret = ::send(client,header.c_str(),::strlen(header.c_str()), 0);
//请求头发送完毕后,服务端将根据请求头中的 Content_length,决定接下来接收的字节数
string fileKey = "--"+BOUNDARY+"\\r\\n";//构建表单中参数,可以发送多个参数,各个参数间以 -- 加上请求头中包含的 BOUNDARY 加上 \\r\\n" 作为分割符
//第一个表单参数构建,表明当前表单数据为文件类型,name表示表单参数名(后台获取参数时的key),filename 表示文件名。"\\r\\n"标识一项请求头的结束
fileKey+="Content-Disposition: form-data; name=\\"file\\"; filename=\\"icon.jpeg\\"\\r\\n";
//标识文件类型,图片类型,\\r\\n\\r\\n 标识表单该项的所有请求头结束
fileKey+="Content-Type: image/jpeg\\r\\n\\r\\n";
//将构建的表单key发送出去
ret = ::send(client,fileKey.c_str(),::strlen(fileKey.c_str()), 0);
//读取文件到字节数组
afile.read(buffer,length);
//发送文件字节,以对应刚发送出去的 表单 key的构建
ret = ::send(client,buffer,length, 0);
//表单数据发送结束的构建,\\r\\n-- 加上 BOUNDARY 加上--\\r\\n的形式
string overString = ("\\r\\n--"+BOUNDARY+"--\\r\\n");
//发送请求体表单结束,如果有多个表单参数,可在此结束标志前继续以上一步形式构建表单参数
ret = ::send(client,overString.c_str(),::strlen(overString.c_str()), 0);
//进行响应接收,这里并未具体多大,只是构建一个较长的字符数组读取
char recv_buf[1024];
int recv_len = recv(client, recv_buf, 1024, 0);
closesocket(client);
cout<<recv_buf<<endl; //打印响应
结果:
客户端打印:
服务端结果:
具体报文:
POST /image/uploadImg HTTP/1.1
Host: localhost:8899
Connection: keep-alive
Accept: */*
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type,username
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36
Content-Length: 157013
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryeQ0uB5bSgAI8zsNS
------WebKitFormBoundaryeQ0uB5bSgAI8zsNS
Content-Disposition: form-data; name="file"; filename="icon.jpeg"
Content-Type: image/jpeg
[图片字节数据,此处省略]
------WebKitFormBoundaryeQ0uB5bSgAI8zsNS--
注意事项
1、content_length是请求体的总字节数大小,此例子是 图片大小 156830 加上表单中头部------WebKitFormBoundaryeQ0uB5bSgAI8zsNS\\r\\n
Content-Disposition: form-data; name="file"; filename="icon.jpeg"\\r\\n
Content-Type: image/jpeg\\r\\n
\\r\\n
以及结束符
\\r\\n
------WebKitFormBoundaryeQ0uB5bSgAI8zsNS--\\r\\n
包含\\r\\n 在内的183个字节。
2、不带Content_length ,springboot会报空指针异常
3、Content_length与body长度不匹配会报 org.apache.tomcat.util.http.fileupload.MultipartStream$MalformedStreamException: Stream ended unexpectedly
意外的流结束
4、http中/r/n是很重要的分割符,表单分隔符(boundary)是自定义的,需要在请求头中注明。
以上是关于原生Socket TCP实现HTTP文件上传的主要内容,如果未能解决你的问题,请参考以下文章