引言
由于本人读的交通类大学的计算机科学,最近有个铁路实习,需要实现一个B/S架构的管理系统,我负责的是后端提供管理数据库的服务。 网上查阅资料后决定用Tomcat实现java服务的提供,但是网上学着学着,发现服务器本质就是通过套接字socket建立IO流传递信息的一个过程。用Tomcat虽然好,但是由于高度分装性,学不到底层原理。本着大学压榨自己的思想(别问,问就是我头铁),就干脆自己写一个简单的仅仅提供HTTP服务的服务器(确实很有成就感,哈哈哈哈哈)。
阅读本篇文章需要的知识有: 只要会java的基本语法就行,笔者本人也只学了一个学期的java (对,我是菜鸡,如果有错误的地方大家在评论区清点喷)
TCP协议介绍
说到大名鼎鼎的TCP协议,肯定马上就有人想到三次握手,四次挥手和滑动窗口等等。但是这些不是我想讲的! 在java高度封装下,只需要知道TCP协议是保证交付的一个协议就行。什么是保证交互呢?就是你发给我一个信息,我一定会告诉你我收到了你的信息,如果我没有告诉你,那么你就要重发请求。 举个简单的例子: A对B说:晚上我们一起去吃烤肉? 情况一: B对A说:好。 A就知道B同意了。 情况二: B没听见A说的什么,什么也没有回应 A不知道B的想法,A再问B:你说什么? 情况三: B对A说:好。 A没有听清楚B说什么。 A再问B:你说什么? 上面三种情况分别对应: 一:成功交付 二:A的包丢了 三:B的包丢了 根据反馈处理机制,A重新发送包,所以实现了互联网可靠传输。 因为Java实现的服务器也是需要在局域网中通信,不排除丢包的可能性,我选择浏览器和服务器传输数据建立的所有连接都是TCP连接,不需要考虑网络丢包的可能性,我们只考虑服务器发送的信息类型及如何发送信息,即实现了网络对我的透明性。
HTTP协议介绍
因为是B/S架构下的服务器,且本人网络基础知识才刚刚学完(由于中途参加了蓝桥杯国赛,偷偷用了几节网络课学算法,好多地方都是自学,感觉学的不太好,内卷失败,哈哈哈),所以就没有用HTTPS来给自己加大游戏难度,自己手动实现了精简的不能再精简的HTTP协议。好,现在步入正题,讲讲HTTP协议。
HTTP: (Hyper Text Transfer Protocol,HTTP)就是一个传输超文本的协议,啥是超文本呢?广义来说,就是信息的载体,包括但不限于视频,音频。HTTP规定了报文的格式,方便浏览器和服务器识别报文的信息。
HTTP报文格式: HTTP报文分为请求报文和响应报文,分别用于客户向服务器发送请求和服务器对请求产生应答。请求报文我用到了GET和POST报文格式,所以对于请求报文我暂时只讲这两种报文根式。GET就是向服务器请求一个静态文件,POST就是向服务器请求一个动态服务。
GET报文:
请求行:GET filename.xxx HTTP/1.1
请求头:Host: xxx.xxx.xxx.xxx:8080
。。。
Upgrade-Insecure-Requests: 1
请求体:message
1.GET报文分为请求行,请求头和请求体三个部分组成。 请求行说明这是一个GET报文,需要服务器向浏览器发送filename.xxx文件,采用的HTTP协议版本为1.1 2.请求头告知了服务器的一些报文的信息 3.请求体就是发送的额外信息, GET是一个请求静态文件的报文,只需要告诉浏览器报文的名字,不需要发送额外信息,所以请求体为空。
POST报文格式:
请求行:POST programname.java HTTP/1.1
请求头:Host: xxx.xxx.xxx.xxx:8080
。。。
Cookie: Idea-dd687ae7=0eebdb6c-d2c6-4dd3-941b-755e9519ebfc
请求体:message
1.POST报文也分为请求行,请求头和请求体三个部分。 请求行说明这是一个POST报文,需要服务器向浏览器提供服务的程序为programname.java,采用的HTTP协议版本为1.1 2.请求头告知了服务器的一些报文的信息 3.请求体就是发送的额外信息, POST是一个请求动态服务的报文,有时候往数据库中存入信息需要发送额外信息,所以请求体一般不为空。 现在就讲完了我们需要用到的请求报文的格式,现在讲讲响应报文的格式。
OK报文
响应行:HTTP/1.1 200 OK
响应头:mytomcat1.Server:apach-Coyote/1.1
Content-Type:text/html;charset=utf-8
响应体:message
OK报文的代码是200,表示的意义是成功找到了目标报文并发送。
NotFound报文
响应行:HTTP/1.1 404 not found
响应头:mytomcat1.Server:apach-Coyote/1.1
Content-Type:text/html;charset=utf-8
响应体:file not found
NotFound报文的代码是404,表示请求的服务没有找到。
套接字Socket介绍
套接字由计算机的IP地址和端口号组成,相当于计算机连接互联网的接口,因为我们需要实现在局域网 (在互联网上通信需要域名,有没有好心人资助我20块钱买个域名,害,太穷了) 上通信,所以必须使用到端口号,端口号有操作系统管理,java也封装了专门的类,所以相对来说对我还是比较友好。
在这里,用java写服务器需要用到的所有理论知识就介绍完成了,现在正式开始写代码。
代码&思路
0.准备工作
这里主要是前期变量的定义,需要注意的是这里对动态服务进行了封装,用服务名称=执行服务的java类的地址 进行映射,再用java反射来实现类,从而提供服务。用到了.properties文件,来实现服务名和服务代码的映射。
public static String WEB_ROOT = System.getProperty("user.dir");
private static String url = "";
private static StringBuffer content = null;
private static Map<String ,String> map=new HashMap<>();
private static String message;
static {
Properties pro = new Properties();
try {
pro.load(new FileInputStream(WEB_ROOT+"\\conf.properties"));
Set set = pro.keySet();
Iterator it = set.iterator();
if (it.hasNext()){
String key = it.next().toString();
String value = pro.getProperty(key);
map.put(key,value);
}
} catch (IOException e) {
e.printStackTrace();
}
}
1.监听端口,等待建立IO流
无论是局域网还是互联网,信息网络中的传输都是以信息流的形式进行传输,所以必须采用IO流的形式来接受和发送信息。思路如下图:
ServerSocket serverSocket;
Socket socket = null;
InputStream is = null;
OutputStream os = null;
serverSocket = new ServerSocket(8080);
while(true) {
socket = serverSocket.accept();
os = socket.getOutputStream();
is = socket.getInputStream();
2.得到请求报文后,分析请求报文
因为网络传输给服务器的是一长串字符串,虽然该字符串为标准的HTTP请求报文格式,但是服务器仍然无法识别,需要手动分析。
if(parse(is)) {
if (url.indexOf(".")!=-1) {
sentStaticResoure(os);
} else {
sentDyResourse(is,os);
}
}
注:判断是否为分手信息是因为浏览器关闭网页时,服务器会接收到一个空信息,而服务器对空信息无法分析,所有会报错,需要分开考虑。
3.服务完成,关闭IO流
在服务完成后,如果不关闭IO流,IO流会对这个端口进行阻塞,导致服务器无法对其他浏览器的请求提供服务。
try {
if (is != null)
is.close();
if (os != null)
os.close();
if (socket != null)
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
4.parse函数解析
这个函数是用来分析请求报文,得到静态服务需要传输的文件名,或者请求的动态服务名称,存入url变量(字符串类型)。
private static boolean parse(InputStream is) {
content = new StringBuffer();
byte[] buffer = new byte[2048];
int len = -1;
try {
len = is.read(buffer);
} catch (IOException e) {
e.printStackTrace();
}
if (len == -1) {
url = null;
return false;
}
for (int i = 0; i < len; i++) {
content.append((char) buffer[i]);
}
System.out.println("content:\n" + content);
url = parseURL(content.toString());
return true;
}
private static String parseURL(String content) {
int index1, index2;
index1 = content.indexOf(" ");
index2 = content.indexOf(" ", index1 + 1);
message = content.substring(content.lastIndexOf("\n")+1);
return content.substring(index1 + 2, index2);
}
5.sentStaticResoure函数解析
这个函数用于向浏览器发送静态数据,比如html文件,jpg图像等等。
private static void sentStaticResoure(OutputStream os) {
byte[] bytes = new byte[100000];
FileInputStream fis = null;
try {
File file = new File(WEB_ROOT, url);
if (file.exists()) {
os.write("HTTP/1.1 200 OK\n".getBytes(StandardCharsets.UTF_8));
os.write("mytomcat1.Server:apach-Coyote/1.1\n".getBytes(StandardCharsets.UTF_8));
String tail = url.substring(url.indexOf(".") + 1);
os.write(("Content-Type:text/" + tail + ";charset=utf-8\n").getBytes(StandardCharsets.UTF_8));
os.write("\n".getBytes(StandardCharsets.UTF_8));
fis = new FileInputStream(file);
int len = fis.read(bytes);
os.write(bytes, 0, len);
os.flush();
} else {
os.write("HTTP/1.1 404 not found\n".getBytes(StandardCharsets.UTF_8));
os.write("mytomcat1.Server:apach-Coyote/1.1\n".getBytes(StandardCharsets.UTF_8));
os.write("Content-Type:text/html;charset=utf-8\n".getBytes(StandardCharsets.UTF_8));
os.write("\nfile not found".getBytes(StandardCharsets.UTF_8));
os.flush();
}
} catch (IOException e) {
System.out.println("文件为空");
e.printStackTrace();
} finally {
try {
if (fis != null) {
fis.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
6.sentDyResourse函数解析
这个函数用于向浏览器发送动态服务内容,
private static void sentDyResourse(InputStream is, OutputStream os) {
try {
os.write("HTTP/1.1 200 OK\n".getBytes(StandardCharsets.UTF_8));
os.write("mytomcat1.Server:apach-Coyote/1.1\n".getBytes(StandardCharsets.UTF_8));
os.write(("Content-Type:text/html;charset=utf-8\n").getBytes(StandardCharsets.UTF_8));
os.write("\n".getBytes(StandardCharsets.UTF_8));
url=url.substring(url.lastIndexOf("/")+1);
Class clazz = Class.forName(map.get(url));
Serable serable = (Serable) clazz.newInstance();
serable.server(message,os);
}catch (Exception e){
e.printStackTrace();
}
}
7.404消息的发送
如果浏览器请求的服务不存在,或者浏览器请求的文件不存在,那么就需要返回一个404信息,告诉浏览器他访问的信息不存在。
os.write("HTTP/1.1 404 not found\n".getBytes(StandardCharsets.UTF_8));
os.write("mytomcat1.Server:apach-Coyote/1.1\n".getBytes(StandardCharsets.UTF_8));
os.write("Content-Type:text/html;charset=utf-8\n".getBytes(StandardCharsets.UTF_8));
os.write("\nfile not found".getBytes(StandardCharsets.UTF_8));
os.flush();
8.举例一个服务
在配置文件中写好这个服务,然后就可以直接调用这个服务了。调用服务的方式就是通过java反射新建aaTest类,然后调用aaTest的方法。
package mytomcat2;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
public class aaTest implements Serable {
public aaTest(){
System.out.println("aa is creating");
}
@Override
public void server(String message, OutputStream os) {
try {
os.write(("我收到了:"+message).getBytes(StandardCharsets.UTF_8));
os.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
然后
按照上面的代码的思路,就可以用自己的电脑实现一个简单的服务器,虽然简陋,但是在写服务器的过程中,最大的收获不是写出来了一个服务器,而是加深了对计算机网络的理解,初步接触了socket编程,还有java反射等等的知识得到了巩固。**这里我没有放数据库的代码,一个是考虑到数据库处理设计到了大量java反射,对于一些对java反射不熟练的同学可能不友好;二是数据库只是java的一种服务,没必要纠结服务类型。**抓住主要矛盾,兼顾次要矛盾,就可以学到很多东西,压力也不会很大。
|