这里是使用的java实现的,每种语言的实现方式都是一样的,拼凑http的报文信息进行发送,以及连接端口号,接收报文信息解析即可。
服务端先随意写个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 解释 参考链接:.
- 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的底层原理(参考链接):