第一版本
效果
访问localhost:8089,返回响应报文给浏览器
服务器收到的请求报文内容
浏览器转圈圈,是因为一直没收到响应报文
public class Test {
public static void main(String[] args) {
//写一个简易服务器
try {//IDEA在8089监听是否有连接请求
ServerSocket serverSocket = new ServerSocket(8089);
Socket socket = serverSocket.accept();//有连接请求会返回socket对象
InputStream in = socket.getInputStream();//获得输入流对象,用来接收tcp报文的数据部分
byte[] data = new byte[1024];//暂存接收数据
int dataCount;//一次接收的数据大小(几个字节)
//不能用while,因为客户端没用socket.shutdownOutput()禁用此套接字的输出流,
// 这样服务端就不会收到null,就会一直跳不出循环
/* while ((dataCount = in.read(data)) != -1) {//接收tcp报文的数据部分
//把字节数组[0,dataCount)的字节转换成字符串
String str=new String(data,0,dataCount);
System.out.println(str);//str是请求报文
}*/
dataCount = in.read(data);
//把字节数组[0,dataCount)的字节转换成字符串
String str = new String(data, 0, dataCount);
System.out.println(str);//str是请求报文
OutputStream out = socket.getOutputStream();//用来发响应报文
StringBuffer buffer = new StringBuffer();//类似string多用于字符拼接
buffer.append("HTTP/1.1 404 Not Found\r\n");
buffer.append("Content-Type:text/html\r\n");
buffer.append("\r\n");
buffer.append("<div style='color:red'>File Not Found</div>");
out.write(buffer.toString().getBytes("utf-8"));//发送tcp响应报文的数据部分
serverSocket.close();
//一定要关闭socket,才能关闭它底层的流对象
// 不然out发送的响应报文一直在缓存区,浏览器接收不到响应报文就会转圈圈
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
第二版本
效果
在浏览器地址栏输入想访问1.html,服务器返回的响应报文里就是1.html
请求2.html,服务器返回的响应报文里就是2.html
如果浏览器请求的资源存在,那么返回200状态码,同时将文件的内容写入到响应体中
如果不存在,那么返回404状态码
http报文的格式
GET /1.html?use=aa HTTP/1.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18363
Accept-Encoding: gzip, deflate
Host: localhost:8089
Connection: Keep-Alive
Cookie: Idea-3faec05=31b22dc0-e261-45ff-9a7d-37a03783321b
Java服务器收到的请求报文
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class Test {
public static void main(String[] args) {
//写一个简易服务器
try {
//IDEA在8089监听是否有连接请求
ServerSocket serverSocket = new ServerSocket(8089);
Socket socket = serverSocket.accept();//有连接请求会返回socket对象
InputStream in = socket.getInputStream();//获得输入流对象,用来接收tcp报文
byte[] data = new byte[1024];//暂存接收数据
int dataCount;//一次接收的数据大小(几个字节)
//不能用while,因为客户端没用socket.shutdownOutput()禁用此套接字的输出流,
// 这样服务端就不会收到null,就会一直跳不出循环
/* while ((dataCount = in.read(data)) != -1) {//接收tcp报文的数据
//把字节数组[0,dataCount)的字节转换成字符串
String str=new String(data,0,dataCount);
System.out.println(str);//str是请求报文
}*/
dataCount = in.read(data);
//把字节数组[0,dataCount)的字节转换成字符串
String str = new String(data, 0, dataCount);
System.out.println(str);//str是请求报文
//------------解析请求报文,得到用户想要的资源------
//http报文的每行后面有一个换行符,可以用这个换行把请求报文分割成多行
int i = str.indexOf(System.lineSeparator());//得到请求行最后的回车所在下标(回车占两个字符)
//获得请求行(不包含回车的),GET /1.html HTTP/1.1
String requestLine = str.substring(0, i);//切割区间[0,i)
//请求行数据之间用空格分隔
String[] parts = requestLine.split(" ");
String method = parts[0];//请求方式
String url = parts[1];//请求URL
String protocol = parts[2];//通信的协议和版本
//判断请求URL有没有携带参数,有就去掉
int j = url.indexOf("?");
if (j != -1) {// 因为?后面的请求参数对用文件名搜索文件没有帮助
url = url.substring(0, j);
}
//去掉url前面的/,/1.html
String fileName = url.substring(1);//截取第一个字符及以后的内容
OutputStream out = socket.getOutputStream();//用来发响应报文
StringBuffer buffer = new StringBuffer();//类似string多用于字符拼接
//在服务器的磁盘中搜索目标文件
File file = new File(fileName);
if (file.exists() && !file.isDirectory()) {//确保文件存在,并且不是一个目录
//制作响应报文
buffer.append("HTTP/1.1 200 OK\r\n");
buffer.append("Content-Type:text/html\r\n");
buffer.append("\r\n");
out.write(buffer.toString().getBytes("utf-8"));//把响应报文头放入输出流缓冲区
FileInputStream fileInput = new FileInputStream(file);
byte[] bytes = new byte[1024];
int length;
while ((length = fileInput.read(bytes)) != -1) {
out.write(bytes, 0, length);//把响应报文body放入输出流缓冲区内
}
fileInput.close();
} else {
//制作响应报文,文件不存在,状态码404
buffer.append("HTTP/1.1 404 Not Found\r\n");
buffer.append("Content-Type:text/html\r\n");
buffer.append("\r\n");
buffer.append("<div style='color:red'>File Not Found</div>");
out.write(buffer.toString().getBytes("utf-8"));//把响应报文放入输出流缓冲区
}
serverSocket.close();
//一定要关闭socket,才能关闭它底层的流对象
// 不然out发送的响应报文一直在缓存区,浏览器接收不到响应报文就会转圈圈
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
第三版本
服务器可以一直接收并处理请求,如果单线程模式下,直接在代码外面加一层while,当程序在处理请求时(恰好里面有Thread.sleep()),有新请求过来,因为没有进程在socket.accept()处一直阻塞监听,会遗漏请求,所有至少有两个线程,主线程负责监听请求,子线程负责处理请求
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class Test {
public static void main(String[] args) {
//写一个简易服务器
try {
//IDEA在8089监听是否有连接请求
ServerSocket serverSocket = new ServerSocket(8089);
Socket socket = serverSocket.accept();//有连接请求会返回socket对象
//每当有一个客户端连接进来,那么就会生成一个新的子线程去处理该客户端的请求信息
new Thread(new Runnable() {
@Override
public void run() {
Request request = new Request(socket);
try {
OutputStream out = socket.getOutputStream();//用来发响应报文
StringBuffer buffer = new StringBuffer();//类似string多用于字符拼接
//在服务器的磁盘中搜索目标文件
File file = new File(request.getRequestResources());
if (file.exists() && !file.isDirectory()) {//确保文件存在,并且不是一个目录
//制作响应报文
buffer.append("HTTP/1.1 200 OK\r\n");
buffer.append("Content-Type:text/html\r\n");
buffer.append("\r\n");
out.write(buffer.toString().getBytes("utf-8"));//把响应报文头放入输出流缓冲区
FileInputStream fileInput = new FileInputStream(file);
byte[] bytes = new byte[1024];
int length;
while ((length = fileInput.read(bytes)) != -1) {
out.write(bytes, 0, length);//把响应报文body放入输出流缓冲区内
}
fileInput.close();
} else {
//制作响应报文,文件不存在,状态码404
buffer.append("HTTP/1.1 404 Not Found\r\n");
buffer.append("Content-Type:text/html\r\n");
buffer.append("\r\n");
buffer.append("<div style='color:red'>File Not Found</div>");
out.write(buffer.toString().getBytes("utf-8"));//把响应报文放入输出流缓冲区
}
//一定要关闭socket,才能关闭它底层的流对象
// 不然out发送的响应报文一直在缓存区,浏览器接收不到响应报文就会转圈圈
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
每个请求报文的格式雷同,可以创建一个Request类,类的成员变量是请求报文解析后的结果
Request类中有专门解析请求报文的方法
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
/*每个请求报文的格式雷同,可以创建一个Request类,类的成员变量是请求报文解析后的结果,
Request类中有专门解析请求报文的方法*/
public class Request {
private Socket socket;
//保存解析之后的HTTP请求报文
private String requestString;
//HTTP请求方法
private String method;
//HTTP请求资源
private String requestResources;
//HTTP请求版本协议
private String protocol;
//map用来存放请求头键值对
private Map<String, String> requestHeaders = new HashMap<>();
public Request(Socket socket) {
this.socket = socket;
try {
InputStream in = socket.getInputStream();//获得输入流对象,用来接收tcp报文
byte[] data = new byte[1024];//暂存接收数据
int dataCount;//一次接收的数据大小(几个字节)
/* 不能用while,因为客户端没用socket.shutdownOutput() 禁用此套接字的输出流,
这样服务端就不会收到null,就会一直跳不出循环*/
/* while ((dataCount = in.read(data)) != -1) {//接收tcp报文的数据
把字节数组[0, dataCount)的字节转换成字符串
String str = new String(data, 0, dataCount);
System.out.println(str);//str是请求报文
}*/
dataCount = in.read(data);
//把字节数组[0,dataCount)的字节转换成字符串
requestString = new String(data, 0, dataCount);
System.out.println(requestString);//requestString是请求报文
//------------解析请求报文------
parseRequestLine();//封装请求行
parseRequestHeaders();//封装请求头
} catch (IOException e) {
e.printStackTrace();
}
}
private void parseRequestLine() {//解析请求报文的请求行
//http报文的每行后面有一个换行符,可以用这个换行把请求报文分割成多行
int i = requestString.indexOf(System.lineSeparator());//得到请求行最后的回车所在下标(回车占两个字符)
//获得请求行(不包含回车的),GET /1.html HTTP/1.1
String requestLine = requestString.substring(0, i);//切割区间[0,i)
//请求行数据之间用空格分隔
String[] parts = requestLine.split(" ");
method = parts[0];//请求方式
String url = parts[1];//请求URL
protocol = parts[2];//通信的协议和版本
//判断请求URL有没有携带参数,有就去掉
int j = url.indexOf("?");
if (j != -1) {// 因为?后面的请求参数对用文件名搜索文件没有帮助
url = url.substring(0, j);
}
//去掉url前面的/,/1.html
requestResources = url.substring(1);//截取第一个字符及以后的内容
}
private void parseRequestHeaders() {//解析请求头
//http报文的每行后面有一个换行符
int i = requestString.indexOf(System.lineSeparator());//得到请求报文第一行的回车所在下标(回车占两个字符)
int j = requestString.indexOf(System.lineSeparator() + System.lineSeparator());//定位到请求头最后一行的回车处
//得到请求头
String substring = requestString.substring(i + 2, j);
String[] heads = substring.split(System.lineSeparator());
//取出请求头的key-value
for (String head : heads) {
int index = head.indexOf(":");
String key = head.substring(0, index);
String value = head.substring(index + 1);
requestHeaders.put(key, value);
}
}
public Socket getSocket() {
return socket;
}
public void setSocket(Socket socket) {
this.socket = socket;
}
public String getRequestString() {
return requestString;
}
public void setRequestString(String requestString) {
this.requestString = requestString;
}
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public String getRequestResources() {
return requestResources;
}
public void setRequestResources(String requestResources) {
this.requestResources = requestResources;
}
public String getProtocol() {
return protocol;
}
public void setProtocol(String protocol) {
this.protocol = protocol;
}
public Map<String, String> getRequestHeaders() {
return requestHeaders;
}
public void setRequestHeaders(Map<String, String> requestHeaders) {
this.requestHeaders = requestHeaders;
}
}