原生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; //打印响应
}

结果:

客户端打印:

原生Socket TCP实现HTTP文件上传_第1张图片
服务端结果:

具体报文:

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)是自定义的,需要在请求头中注明。

你可能感兴趣的:(http,tcp/ip,http)