这里是使用的java实现的,每种语言的实现方式都是一样的,拼凑http的报文信息进行发送,以及连接端口号,接收报文信息解析即可。
一. socket实现http get请求发送数据
- 服务端先随意写个get请求的接口
@GetMapping("/get")
public Map<String, Object> get(@RequestParam("name") String name){
System.out.println(name);
return Map.of("msg","success");
}
- 要用原生的socket去调用http接口,就需要按照http的规范去发送报文信息;通过ApiPost工具发送请求,再使用抓包工具可以拿到报文信息(大概长这样):
(我们只需要关注其中比较重要的几项即可)
3. 先写一个通用的发送socket数据的方法
private void sendHttp(HttpSendVo vo, String httpBody){
try {
System.out.println("**************** Server request ****************");
System.out.println(httpBody);
Socket socket = new Socket(vo.getIp(), vo.getPort());
// socket.setSoTimeout(30000);
PrintWriter outWriter = new PrintWriter(socket.getOutputStream());
outWriter.println(httpBody);
outWriter.flush();
// socket客户端接收tomcat返回的数据
BufferedReader inReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// 以下是服务器返回的数据
System.out.println("**************** Server response ****************");
String tmp = "";
while ((tmp = inReader.readLine()) != null) {
// 解析服务器返回的数据,做相应的处理
System.out.println(tmp);
}
outWriter.close();
inReader.close();
} catch (Exception e) {
e.printStackTrace();
}
}
- 使用硬编码方法,将上面的http报文传过去做测试,是否能够正常请求成功(下面的post也可以这样先来进行测试,确保有一份能跑通的报文信息,方便拼接报文时做对比)
直接调用sendHttp(),传入上面报文测试是否通过即可,需要注意content-length不能出错
- 然后依据上面的报文拼接代码如下(这里只需要加上主要的几个请求头即可):
public static void sendGet(HttpSendGetVo vo) {
try {
StringBuffer httpBody = new StringBuffer(200);
httpBody.append("GET "+vo.getUrl()+"?"+vo.getParams()+" HTTP/1.1\r\n");
httpBody.append("Host: "+vo.getIp()+":"+vo.getPort()+"\r\n"); // 这个host参数也是必须的
httpBody.append("Connection: Close\r\n"); // 这个不加,client不会关闭
httpBody.append("\r\n");
sendHttp(vo, httpBody.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
- 此处的字符是都不能出错的,否则便发送不成功,所以可以用字符串作比较的方式排查问题:
private void compareStr(StringBuffer sb){
String s = sendData();
String s2 = sb.toString();
for(int i = 0; i < s.length(); i ++){
char c = s.charAt(i);
char c2= s2.charAt(i);
System.out.println(c+" "+c2+" "+i);
if(c != c2){
System.out.println("不同:: "+c+" "+c2+" "+i);
}
}
System.out.println( sendData().toString());
System.out.println(sb.toString()+ " "+sb.length());
System.out.println(sb.toString().equals(sendData().toString()));
}
7.发送上面的请求,报文如下:
* GET /file/get?name=111 HTTP/1.1
* Host: 127.0.0.1:8080
* Connection: Close
*
*
* HTTP/1.1 200
* Content-Type: text/plain;charset=UTF-8
* Content-Length: 3
* Date: Sat, 12 Nov 2022 05:01:50 GMT
* Connection: close
二. socket实现http post请求发送数据
get请求还是比较简单的,而post请求有多种传参方式,并且还有文件传输,报文拼接就相对复杂一些。
- post中 content-type 解释
参考链接:https://blog.csdn.net/Eat_a_meal/article/details/124198611.- multipart/form-data:就是http请求中的multipart/form-data,它会将表单的数据处理为一条消息,以标签为单元,用分隔符分开。既可以上传键值对,也可以上传文件。当上传的字段是文件时,会有Content-Type来表名文件类型;content-disposition,用来说明字段的一些信息;由于有boundary隔离,所以multipart/form-data既可以上传文件,也可以上传键值对,它采用了键值对的方式,所以可以上传多个文件。
- application/x-www-form-urlencoded:就是application/x-www-form-urlencoded,会将表单内的数据转换为键值对,比如,username=张三
- raw:以上传任意格式的文本,可以上传text、json、xml、html等content-type= text/html(HTML 文档);text/plain(纯文本);text/css(CSS 样式表);application/json (json字符串)
- binary:相当于Content-Type:application/octet-stream,从字面意思得知,只可以上传二进制数据,通常用来上传文件,由于没有键值,所以,一次只能上传一个文件。
- 准备一个post的接口,其中包含接收参数和文件
@RequestMapping("/test")
public Map<String, Object> test(
@RequestParam(name="file",required = false)MultipartFile file, @RequestParam Map param) throws IOException {
if(null != file){
file.transferTo(new File("E:\\"+file.getOriginalFilename()));
System.out.printf("上传文件名:【%s】",file.getOriginalFilename());
}
if(null != param){
System.out.printf("上传参数:【%s】",JSONObject.toJSONString(param));
}
return Map.of("msg","success");
}
- post使用 x-www-form-urlencoded 发送数据
3.1 报文如下(从中可以看出参数是&拼接的,并且报文信息比较简单):
POST /file/text HTTP/1.1
Host: 127.0.0.1
Content-Length: 22
Content-Type: application/x-www-form-urlencoded
Connection: Close
name=gloomyfish&age=32
3.2 然后依据上面的报文拼接代码如下(这里只需要加上主要的几个请求头即可,此处需要注意Content-Length的长度,必须是发送数据的长度):
public static void sendPost(HttpSendPostVo vo) throws IOException {
String data = URLEncoder.encode("name", "utf-8") + "="
+ URLEncoder.encode("哈哈哈哈", "utf-8")
+ "&" + URLEncoder.encode("age", "utf-8") + "="
+ URLEncoder.encode("32", "utf-8");
StringBuffer sb = new StringBuffer();
sb.append("POST " + vo.getUrl() + " HTTP/1.1\r\n");
sb.append("Host: " + vo.getIp() + "\r\n");
sb.append("Content-Length: " + data.length() + "\r\n");
sb.append("Content-Type: "+ContentTypeEnum.FORM_URLENCODED.getContentType()+"\n");
sb.append("Connection: Close\r\n");
sb.append("\r\n");
sb.append(data);
sb.append("\r\n");
sendHttp(vo, sb.toString());
}
-
post使用 form-data 发送数据
- 无文件上传()
a)报文如下(这里就需要注意拼接和换行了,必须保证符合协议规范):
POST /file/text HTTP/1.1 Host: 127.0.0.1 Content-Type: multipart/form-data; boundary=qyl Content-Length: 113 Connection: Close --qyl Content-Disposition: form-data; name="id" 111 --qyl Content-Disposition: form-data; name="name" 222 --qyl--
b)发送请求代码如下(,此处需要注意Content-Length的长度,必须是发送数据的长度):
public static void sendPostFormDataNoFile(HttpSendPostVo vo) throws IOException { String boundary = "qyl"; StringBuffer data = new StringBuffer(); data.append(getFormItem("id", "11")); data.append(getFormItem("name", "11")); data.append("--"+boundary+"--"); StringBuffer sb = new StringBuffer(); sb.append("POST " + vo.getUrl() + " HTTP/1.1\r\n"); sb.append("Host: " + vo.getIp() + "\r\n"); sb.append("Content-Type: "+ContentTypeEnum.FORM_DATA.getContentType()+";boundary="+boundary +"\r\n"); sb.append("Content-Length: " + data.length() + "\r\n"); sb.append("Connection: Close\r\n"); sb.append("\r\n"); sb.append(data); sb.append("\r\n"); sendHttp(vo, sb.toString()); }
- 使用文本方式(text/plain)进行上传
a)报文如下(这里需要注意文件的拼接方式,必须保证符合协议规范,此处使用文本类型的文件):
POST /file/text HTTP/1.1 Host: 127.0.0.1 Content-Type: multipart/form-data;boundary=qinyulin Content-Length: 248 Connection: Close --qinyulin Content-Disposition: form-data; name="id" 11 --qinyulin Content-Disposition: form-data; name="name" 11 --qinyulin Content-Disposition: form-data; name="file"; filename="text.txt" Content-Type: text/plain sdfs --qinyulin--
b)发送请求代码如下(,此处需要注意Content-Length的长度,必须是发送数据的长度):
public static void sendPostFormDataWithFile(HttpSendPostVo vo) throws Exception { String boundary = "qinyulin"; StringBuffer data = new StringBuffer(); data.append(getFormItem("\"id\"", "11")); data.append(getFormItem("\"name\"", "11")); data.append(getFormFileItem("\"file\"", "D:\\text.txt",boundary)); data.append("--"+boundary+"--"); StringBuffer sb = new StringBuffer(); sb.append("POST " + vo.getUrl() + " HTTP/1.1\r\n"); sb.append("Host: " + vo.getIp() + "\r\n"); sb.append("Content-Type: "+ContentTypeEnum.FORM_DATA.getContentType()+";boundary="+boundary +"\r\n"); sb.append("Content-Length: " + data.length() + "\r\n"); sb.append("Connection: Close\r\n"); sb.append("\r\n"); sb.append(data); sb.append("\r\n"); sendHttp(vo, sb.toString()); }
- 使用二进制流方式(content-type要写具体类型,或者octet-stream) 进行上传
a)报文如下(这里需要注意文件的拼接方式,必须保证符合协议规范):
POST /file/text HTTP/1.1 Host: 127.0.0.1 Content-Type: multipart/form-data;boundary=qinyulin Content-Length: 21239 accept-encoding: gzip, deflate, br --qinyulin Content-Disposition: form-data; name=name 222 --qinyulin Content-Disposition: form-data; name=id 111 --qinyulin Content-Disposition: form-data; name=file; filename=1668245082534.png Content-Type: image/pngConnection: Keep-Alive �PNG IHDR � ... 这里都是文件的二进制乱码省略了 --qinyulin--
b)发送请求代码如下(,此处需要注意Content-Length的长度,必须是发送数据的长度),这里拼接的时候,文件是byte类型直接传输的:
public void sendPostFormDataWithFileStream(HttpSendPostVo vo) throws Exception { Socket socket = new Socket(vo.getIp(), vo.getPort()); // 创建socket OutputStream out = socket.getOutputStream(); // 创建输出流 File file = vo.getFile(); FileInputStream fin = new FileInputStream(file); byte[] fileBytes = fin.readAllBytes(); // 得到文件字节码 StringBuffer fileBefore = new StringBuffer(); // 输出文件字节码之前的表单数据 vo.getBodyParam().forEach((k, v) ->{ // 表单参数 fileBefore.append(getFormItem(k, v.toString())); }); fileBefore.append("--").append(BOUNDARY).append(NEW_LINE); // 包装文件 fileBefore.append("Content-Disposition: form-data; name=").append(vo.getFileReqName()) .append("; filename=").append(file.getName()).append(NEW_LINE); fileBefore.append("Content-Type: "+Files.probeContentType(file.toPath())); fileBefore.append("Connection: Keep-Alive"); fileBefore.append(NEW_LINE).append(NEW_LINE); String end = getReqEndLine(); // 结尾 StringBuffer sb = new StringBuffer(); sb.append("POST ").append(vo.getUrl()).append(" HTTP/1.1").append(NEW_LINE); sb.append("Host: ").append(vo.getIp()).append(NEW_LINE); sb.append("Content-Type: ").append(ContentTypeEnum.FORM_DATA.getContentType()) .append(";boundary=").append(BOUNDARY).append(NEW_LINE); int length = fileBefore.toString().getBytes().length + fileBytes.length + end.getBytes().length; sb.append("Content-Length: ").append(length).append(NEW_LINE); // sb.append("Content-Length: 21397 \r\n"); // sb.append("Accept:text/html, application/xhtml+xml, */*").append("\r\n"); sb.append("accept-encoding: gzip, deflate, br").append(NEW_LINE).append(NEW_LINE); out.write(sb.toString().getBytes()); // 下面是数据 out.write(fileBefore.toString().getBytes()); out.write(fileBytes); // byte b[]=new byte[1024]; // int rb = 0; // while((rb = fin.read(b))!=-1){ // out.write(b,0,rb); // }; out.write("\r\n".getBytes(StandardCharsets.UTF_8)); out.write(end.getBytes(StandardCharsets.UTF_8)); // socket客户端接收tomcat返回的数据 BufferedReader inReader = new BufferedReader(new InputStreamReader(socket.getInputStream())); // 以下是服务器返回的数据 System.out.println("**************** Server response ****************"); String tmp = ""; while ((tmp = inReader.readLine()) != null) { // 解析服务器返回的数据,做相应的处理 System.out.println(tmp); } fin.close(); out.close(); // inReader.close(); }
- 无文件上传()
-
使用socket服务端接收http请求,代码如下,client发送数据后,用socket监听8080端口,就拿到了所有的报文,然后拿到的数据根据http协议解析就可以了:
System.out.println("start...");
//创建服务器端对象
ServerSocket server = new ServerSocket(PORT);
while(true) {
try {
Socket socket = server.accept();
//创建文件输出流和网络输入流
BufferedReader inReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// String res = inReader.readLine();
// 以下是服务器返回的数据
System.out.println("**************** Server request ****************");
String tmp = "";
while ((tmp = inReader.readLine()) != null) {
// 解析服务器返回的数据,做相应的处理
// res += tmp;
System.out.println(tmp);
// if(tmp.startsWith("Connection")){
// break;
// }
}
// System.out.println("拿到的数据:"+res);
PrintWriter outWriter = new PrintWriter(socket.getOutputStream());
outWriter.println("success");
outWriter.flush();
outWriter.close();
inReader.close();
}catch (Exception e){
e.printStackTrace();
}
}
代码位置:https://gitee.com/ClumsyBird/learn-demo/tree/master/file-upload
(代码比较粗糙,在com.qyl.http_upload.finally_code包下)
三. 总结
- 通过对于http的实现,能够了解其本质,只是对socket进行了规范制定
- 能够更清楚每种传参方式的优劣,以及什么样的传参对应什么样的接收方式
- 在一些性能比较有影响的地方,在熟悉http之后,能够做一些优化和更高效的使用
- 在一些特殊场景中,能够去自定义私有协议进行传输
- 通过抓包、解析的过程,能够提升爬虫的能力,以及对于传输安全性措施的提升
- java socket的底层原理(参考链接):https://blog.csdn.net/weixin_43719752/article/details/126751394