简单模拟webserver
准备知识
本章节将按照以下内容阐述
一、网络中进程之间是如何通信
二、两种通信协议:TCP、UDP
三、什么是Socket
四、模拟一个webserver
五、请求与响应
六、代码
网络中进程之间是如何通信?
通信嘛,我理解的就是两个进程之间互相收发数据,就像打电话一样。那么进程之间是如何通信的呢,可以参考博客https://www.cnblogs.com/CheeseZH/p/5264465.html
要实现进程通信,首先解决的问题就是如何唯一标识一个进程,否则通信无从谈起,我得知道你的电话号码才能给你打电话吧!那么在本地,可以通过进程PID来标识一个进程(PID熟悉吧,linux下可以使用kill -9 PID 来杀死一个进程),而在网络中如何标识一个进程呢,TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。
使用TCP/IP协议的应用程序通常采用应用编程接口:UNIX BSD的套接字(套接字也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同主机之间的进程通信。)来实现网络进程之间的通信。
两种通信协议:TCP、UDP
TCP是Tranfer Control Protocol的简称,是一种面向连接的保证可靠传输的协议。通过TCP协议传输,得到的是一个顺序的无差错的数据流。发送方和接收方的成对的两个socket(理解为电话的一端)之间必须建立连接,以便在TCP协议的基础上进行通信,当一个socket(通常都是server socket)等待建立连接时,另一个socket可以要求进行连接,一旦这两个socket连接起来,它们就可以进行双向数据传输,双方都可以进行发送或接收操作。
UDP是User Datagram Protocol的简称,是一种无连接的协议,每个数据报都是一个独立的信息,包括完整的源地址或目的地址,它在网络上以任何可能的路径传往目的地,因此能否到达目的地,到达目的地的时间以及内容的正确性都是不能被保证的。(与本主题无关,了解即可)
什么是Socket?
网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket,又称"套接字"。其实我理解的套接字,就是电话的一端。
Socket通讯的过程
Server端监听某个端口是否有连接请求,Client端向Server端发出连接请求,Server端向Client端发回Accept(接受)消息。一个连接就建立起来了。Server端和Client 端都可以通过Send,Write等方法与对方通信。
ServerSocket(服务端)编程步骤
1) 创建套接字,将套接字绑定到一个本地地址和端口上
2) 将套接字设置为监听模式,准备接受客户请求
3) 等客户请求到来时,接受连接请求,返回一个新的对应此连接的套接字,启动线程为当前的连接服务。
4) 返回,等另一个客户请求
5) 关闭套接字
6) Socket客户端编程步骤
7) 1.创建套接字
8) 2.向服务器发起请求
9) 和服务器进行通信
10) 关闭套接字
创建Socket
java在包java.net中提供了两个类Socket和ServerSocket,分别用来表示双向连接的客户端和服务端。这是两个封装得非常好的类,使用很方便。服务端就是一个ServerSocket,这里,我们主要讲ServerSocket
ServerSocket的构造函数有:
a. ServerSocket()throws IOException
b. ServerSocket(int port)throws IOException
c. ServerSocket(int port, int backlog)throws IOException
d. ServerSocket(int port, int backlog, InetAddress bindAddr)throws IOException
参数说明:
a. port - 端口号
b. backlog - 指的是最大等待队列长度,超过这个数量的客户端链接过来就不会让它等待而是直接拒绝了
c. bindAddr -用于绑定服务器IP,InetAddress用来描述主机地址
注意:
如果端口被占用或者没有权限使用某些端口会抛出BindException错误。譬如1~1023的端口需要管理员才拥有权限绑定。
如果设置端口为0,则系统会自动为其分配一个端口;
backlog参数将覆盖操作系统限定的队列的最大长度. 值得注意的是, 在以下几种情况中, 仍然会采用操作系统限定的队列的最大长度:
a. backlog 参数的值大于操作系统限定的队列的最大长度,
b. backlog 参数的值小于或等于0,
c. 在ServerSocket 构造方法中没有设置 backlog 参数。
常用方法:
accept()
侦听并接受到此套接字的连接。
getInetAddress()
返回此服务器套接字的本地地址。
模拟一个webserver
web server原理很简单,在服务器上运行一个ServerSocket程序,这个程序可以监控端口。比如有一个服务器的IP是10.10.40.12,其中的一个程序myserver监控的是8080端口,当浏览器发送一个请求时,这个请求里包含了所需要的页面信息index,(如: http://10.10.40.12:8080/index),请求传到服务器,myserver程序会接受这个请求,并解析请求,(因为请求就是IO流,所以可以用JAVA中的IO操作来解析),解析后,找到请求所需要的页面,页面其实就是一个文件,用JAVA的文件操作,读出这个页面,然后通过Socket写到客户端,就可以了。
简单来说访问网站的过程是这个:用户在浏览器输入一个地址,摁回车之后,就会向服务器发送一个请求,告诉服务器它想要一个页面,服务器找到这个页面,然后给浏览器发送回来。
请求与响应
请求,当客户端发起请求时,我的理解就是客户端向浏览器发送了个字符串,这个字符串包含了一些信息,比如:
POST /examples/default.jsp HTTP/1.0
Accept: text/plain;text/html
Accept-Language: en-gb
Connection:Keep-Alive
Host: localhost
User-Agent: Mozilla/4.0(compatible;MSIE:4.01;Widows 98)
Content-Length: 33
Content-Type:application/x-www-form-urlencoded
Accept-Encodding:gzip,deflate
lastName=Franks&firstName=Michael
第一行是请求方法 请求资源路径 协议/协议版本,中间那部分是请求头,最后一行是请求体,这里用的POST方法,所以表单的参数会在请求体中,这行和上面的用一个空行隔开,以便解析此请求的时候能知道往下就是请求体了。
响应。服务器接收到用户的请求之后,会找到用户所要的资源(页面,图片等),再发送到客户端。响应的格式如下:
HTTP/1.1 200 OK
Server: Microsoft-IIS/4.0
Date: Mon, 5 Jan 2004 13:13:33 GMT
Content-Type: text/html
Last-Modified: Mon, 5 Jan 2004 13:13:12 GMT
Content-Length: 112
<html>
<head>
<title>HTTP Response Example</title>
</head>
<body>
Welcome to Software
</body>
</html>
第一行的意思:协议/协议版本 状态码(成功,失败等状态用数字来表示) 状态描述,接下来是告诉客户端的一些信息,再接下来就是客户请求的页面的内容。
下面写代码
先看一下流程
从图中看出我们需要创建以下类来完成:
1. HttpServer 用来创建ServerSocket
2. Dispatcher 用来分发请求
3. Reqest 用来获取请求信息
4. Response 用来向客户端发送响应信息
另外,为了方便,我们还需要些一个Util类,用来写一些工具
注意:从Socket获取的inputstream/outputStream 执行关闭操作时,Socket也会关闭,所以我们要在inputStream和outputStream都完成工作时,一起关闭。
HttpServer:
package com.cn.webserver;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class HttpServer {
public boolean shutdown = false;
public static void main(String[] args) {
try {
new HttpServer().start();
} catch (IOException e) {
e.printStackTrace();
}
}
public void start() throws IOException {
int port = 8081;
int backlog = 1000;
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(port, backlog,InetAddress.getByName("127.0.0.1"));
while (!shutdown) {
Socket socket = serverSocket.accept();
ExecutorService es = Executors.newCachedThreadPool();
es.execute(new Dispatcher(socket));
}
} catch (IOException e) {
shutdown = true;
serverSocket.close();
e.printStackTrace();
}
}
}
Dispatcher:
package com.cn.webserver;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import com.cn.request.Request;
import com.cn.response.Response;
import com.cn.util.Util;
public class Dispatcher implements Runnable{
private Socket socket;
private InputStream is;
private OutputStream os;
private int code = 200;
public Dispatcher(Socket socket){
this.socket = socket;
try {
this.is = socket.getInputStream();
this.os = socket.getOutputStream();
} catch (IOException e) {
code = 500;
e.printStackTrace();
}
}
@Override
public void run() {
try {
Request res = new Request(is);
Response resp = new Response(os);
resp.push(res.getUrl(), code);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
Util.closeIO(is);
Util.closeIO(os);
}
}
}
Request:
package com.cn.request;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.HashMap;
import java.util.Map;
import com.cn.util.Util;
public class Request {
private InputStream in;
private Map parameterMap;
private String url;
private String method;
public Request(InputStream in){
this.in = in;
this.parameterMap = new HashMap();
parse();
}
private void parse() {
String requestHead = read();
if(requestHead==null||requestHead.length()<=0){
return ;
}
String firstLine = requestHead.substring(0, requestHead.indexOf("\r\n"));
int a = requestHead.indexOf("/");
this.method = requestHead.substring(0, a).trim();
String urlparam = null;
String parameter = null;
urlparam = firstLine.substring(a,requestHead.indexOf("HTTP/"));
if(method.equalsIgnoreCase("post")){
parameter = requestHead.substring(requestHead.lastIndexOf("\r\n"));
this.url = urlparam;
}else{
if(firstLine.contains("?")){
String []params = urlparam.split("[?]");
this.url = params[0];
if(params.length>0){
String []params1 = params[1].split("&");
if(params1.length>0){
String[] param;
String key = null;
String value = null;
for(int i = 0; i < params1.length;i++){
param = params1[i].split("=");
if(param.length>0){
key = param[0];
if(param.length>1){
value = param[1];
}else{
value = null;
}
this.parameterMap.put(key, value);
}
}
}
}
}else{
this.url=urlparam;
}
}
}
/**
* 获取请求信息
* @return
*/
private String read() {
StringBuffer sb = new StringBuffer();
Reader re = null;
try {
re = new InputStreamReader(this.in, "UTF-8");
char []cbuf = new char[20480];
int index = 0;
index = re.read(cbuf);
if(index>0){
sb.append(cbuf,0,index);
}
} catch (Exception e) {
e.printStackTrace();
}
return sb.toString();
}
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public String getParameterMap(String key) {
return (String)parameterMap.get(key);
}
public void setParameterMap(Map parameterMap) {
this.parameterMap = parameterMap;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
}
Response:
package com.cn.response;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.Date;
import com.cn.util.Util;
public class Response {
private OutputStream out;
private int code;
private StringBuffer context ;
public Response(OutputStream out){
this.out = out;
context = new StringBuffer();
}
public void push(String url,int code){
this.read(url);
String head = this.createHead(context.length());
try {
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(this.out,"UTF-8"));
bw.append(head);
bw.append(this.context);
bw.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
private void read(String url){
if(url==null||url.length()<0){
return;
}
url = url.substring(1);
url = System.getProperty("user.dir")+File.separatorChar+"WebRoot"+File.separatorChar+url;
File file = new File(url);
BufferedReader br = null;
if(!file.exists()){
this.code =404;
this.context.append("<h1>大写的<span style='font-size:90px'>404</span></h1>");
}else{
try {
br = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"));
String cacheStr = br.readLine();
while(cacheStr!=null){
context.append(cacheStr);
cacheStr = br.readLine();
}
} catch (FileNotFoundException e) {
this.code =404;
e.printStackTrace();
} catch (IOException e){
e.printStackTrace();
}finally{
Util.closeIO(br);
}
}
}
private String createHead(int len){
StringBuffer headInfo = new StringBuffer();
headInfo.append("HTTP/1.1").append(" ").append(code).append(" ");
switch (code) {
case 200:
headInfo.append("ok");
break;
case 404:
headInfo.append("NOT FOUND");
break;
case 505:
headInfo.append("SERVER ERROR");
break;
}
headInfo.append("\r\n");
//响应头
headInfo.append("Server: Webserver").append("\r\n");
headInfo.append("Date:").append(new Date()).append("\r\n");
headInfo.append("content-type:text/html;charset=UTF-8").append("\r\n");
//正文长度:字节长度
headInfo.append("Content-Length:").append(len).append("\r\n");
//分隔符
headInfo.append("\r\n");
return headInfo.toString();
}
}
Util:
package com.cn.util;
import java.io.Closeable;
import java.io.IOException;
public class Util {
public static <T extends Closeable> void closeIO(T ...ios){
for(Closeable c :ios){
try {
if(c!=null){
c.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
运行效果:
页面不存在的情况: