java开发一枚,想用qt写一个简单的桌面应用(qt可以直接打包成exe,java却要jre环境)。苦于不熟悉c++,因此考虑用原生socket实现调用http接口。此文针对客户端进行讲解实现。
@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--
------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)是自定义的,需要在请求头中注明。