在浏览器输入一个段网址,就会出现你想要的网页或数据,那么这个过程是如何做到的,今天我们来探索这个过程。首先http是基于socket的封装的,那我们就用socket来实现一个简易的web服务器。
首先先说思路,既然想要基于socket实现web服务器,就要知道在输入网址后都发生了什么,所以我们用springboot写个简易的web服务用Wireshark抓个包看看
java
@Controller
@ResponseBody
public class Test {
@GetMapping("")
public String ccc(){
return "hello word";
}
}
我们就是实现这样的一个建议的web服务器来探讨socket请求的时候都发生了什么
我们来抓个包:
Wireshark抓包
分析请求的过程
首先是tcp连接的三次握手
建立一个TCP连接时:
- 客户端通过调用connect发起打开连接。这时客户端发送SYN(同步)字节,它告诉服务器客户端将在(待连接)连接中发送的数据的初始序号。
- 服务器必须确认(ACK)客户端的SYN,同时自己也发个SYN的字节,它含有服务器将在同一连接中发送的数据的初始序列号。
- 客户端必须确认服务器的SYN
Wireshark抓包分析握手
发送http请求服务器端的过程
客户端发送服务器端
服务器发返回客户端
这里重点看服务器返回的数据,利用socket仿造一下返回数据就可以
这里可以看到主要的参数有响应头和数据
响应头:
bash
HTTP/1.1 200 /r/n http 协议 http状态码
Content-Type: text/html;charset=UTF-8 /r/n 类型
Content-Length: 10/r/n 数据长度
Date: Sun, 22 Aug 2021 13:20:36 GMT/r/n 时间
Keep-Alive: timeout=60rn 长连接时间
Connection: keep-alive/r/n 长连接
/r/n
响应头结束
然后是返回数据
arduino
Line-based text data: text/html (1 lines)
hello word
也就是说服务器主要返回是这几行数据
bash
HTTP/1.1 200 /r/n
Content-Type: text/html;charset=UTF-8/r/n
Content-Length: 10/r/n
/r/n
hello word
Socket连接流程
代码
思路和方法都有了接下来开始写代码
C++
//linux端c++的写法 因为是阻塞式socket所以在读取的时候可能会发生阻塞
#include <iostream>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
using namespace std;
int main(){
int server_socketfd,client_socketfd; //声明服务器端与客户端的socket
struct sockaddr_in my_addr,remote_addr; // 定义socket地址的结构体
int sin_size;
char buf[BUFSIZ]; //数据传送的缓冲区
memset(&my_addr, 0, sizeof(my_addr));
my_addr.sin_family = AF_INET; //socket为TCP/IP -- IPv4
my_addr.sin_addr.s_addr = INADDR_ANY; //指定0.0.0.0的地址 接受所有地址的tcp请求
my_addr.sin_port = htons(8080); //绑定端口
if ((server_socketfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { //定义socket
perror("socket error");
return 1;
}
if (bind(server_socketfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) < 0) {//绑定端口与地址
perror("bind error");
return 1;
}
if (listen(server_socketfd, 5) < 0) { //开启监听
perror("listen error");
return 1;
}
while (true) {
sin_size = sizeof(struct sockaddr_in);
if ((client_socketfd = accept(server_socketfd, (struct sockaddr *)&remote_addr, (socklen_t *)&sin_size)) < 0) { //阻塞等待请求
perror("accept error");
return 1;
}
string data = "hello word"; //构造数据
string buff = "HTTP/1.1 200 \r\n"; //构造头
buff += "Content-Type: text/html;charset=UTF-8 \r\n";
buff += "Content-Length: " + data.length();
buff += "\r\n\r\n";
buff += data;
cout << "accept client:" << inet_ntoa(remote_addr.sin_addr)<< endl; //打印请求客户端IP
int len=recv(client_socketfd, buf, BUFSIZ, 0); //接受客户端的数据并将其输出
buf[len] = '';
cout << buf << endl;
send(client_socketfd, buff.c_str(), buff.length(), 0); //发送请求头与数据
close(client_socketfd); //关闭客户端socket
}
close(server_socketfd);//关闭服务端socket
return 0;
}
java
//java的写法 java总有访问失败的问题怀疑跟socket连接池有关
package com.company;
import java.net.ServerSocket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) throws Exception {
// write your code here
ExecutorService executorService= Executors.newCachedThreadPool(); //构造线程池
ServerSocket serverSocket = new ServerSocket(8080); //绑定端
while (true) {
executorService.execute(new Https(serverSocket.accept()));//监听将监听的分配给线程池
}
}
}
//线程
package org.company;
import java.io.DataOutputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
public class Https extends Thread {
private Socket socket;
public Https(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
OutputStream outputStream = null;
DataOutputStream dataOutputStream = null;
try {
outputStream = socket.getOutputStream(); //获取socket输出流
dataOutputStream = new DataOutputStream(outputStream); //将输出流转换成DataOutputStream
String data = "hello word"; //构造数据
String message = String.format("HTTP/1.1 200\r\n" +
"Content-Type: text/html;charset=UTF-8\r\n" +
"Content-Length: %d\r\n\r\n" +
"%s", data.length(), data);//响应头
dataOutputStream.write(message.toString().getBytes(StandardCharsets.UTF_8)); //写入客户端数据
dataOutputStream.flush(); //清空缓冲区
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
dataOutputStream.close();
outputStream.close();
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
python
#python的写法
import socket
if __name__ == '__main__':
socket_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #定义服务端socket
socket_server.bind(('127.0.0.1', 8080)) #绑定端口与地址
socket_server.listen(1) ##监听一个请求
while 1:
conn, socket_client = socket_server.accept() #阻塞等待请求并赋值给客户端
print(socket_client) #打印客户端IP
data = conn.recv(10240).decode('UTF-8')#解析数据
print(data)#输出数据
data = 'hello word'#返回的参数
buff = 'HTTP/1.1 200 \r\n'
'Content-Type: text/html;charset=UTF-8 \r\n'
'Content-Length: {}\r\n'
'\r\n'
'{}'.format(len(data), data)#构造请求头
conn.send(buff.encode('UTF-8')) #发送数据
conn.close()#关闭客户端的socket请求
socket_server.close() #关闭服务端的socket请求
这个简易的web服务器就算写完了但太简单,不可以区分路径,而且都是阻塞的socket。接下来就需要根据不通的请求路径返回不通的参数与结果并利用非阻塞的socket。通过正则可以获取请求的路径与请求的方式,静态文件可以获取文件内容的方式返回给客户端。
非阻塞的socket可以查看我的这篇文章:基于socket实现一个简易的web服务器------非阻塞的模式
参考文章列表:
- 小草csm:python socket 接口
- UNIX网络编程 卷1:套接字联网API
- Java Socket发送与接收HTTP消息简单实现