在熟悉java se 之后,我们可以试着去看一些开源的东西,比如tomcat。
本章目标:
A. 了解HTTP协议
B. 熟悉Socket类和ServerSocket类
C. 编写一个简单的web server
1.1HTTP协议
http协议就是客户端和服务器之间交互必须遵循的一个规范。它的标准定义如下:
一种详细规定了浏览器和万维网服务器之间互相通信的规则,通过因特网传送万维网文档的数据传送协议。
问题:既然TCP/UDP是广泛使用的网络通信协议,那为啥有多出个http协议来呢?
UDP协议具有不可靠性和不安全性,显然这很难满足web应用的需要。而TCP协议是基于连接和三次握手的,虽然具有可靠性,但有一定的缺陷。试想一下,普通的C/S架构软件,顶多上千个Client同时连接,而B/S架构的网站,十万人同时在线也是很平常的事儿。如果十万个客户端和服务器一直保持连接状态,那服务器如何满足承载呢?这就衍生出了http协议。基于TCP的可靠性连接。通俗点说,就是在请求之后,服务器端立即关闭连接、释放资源。这样既保证了资源可用,也吸取了TCP的可靠性的优点。正因为这点,所以大家通常说http协议是“无状态”的,也就是“服务器不知道你客户端干了啥”,其实很大程度上是基于性能考虑的。以至于后来有了session之类的玩意
1.1.1HTTP请求
一个http请求,包含以下三部分:
A请求方法
B 请求头
C 实体
在研究http协议时,推荐大家使用一款叫作httpwatch的工具。(遗憾的是,该工具是收费的。该咋办就咋办,你懂的)。安装完成后,可以在IE浏览器的tools中直接打开(目前也支持firefox)。
下面是我用httpwatch监视http请求的图例:
点击“Stream”:
GET/rest/general/mnt/server/listgroup HTTP/1.1
Accept: image/gif, image/jpeg,image/pjpeg, image/pjpeg, application/x-shockwave-flash,application/x-ms-application,application/x-ms-xbap,application/vnd.ms-xpsdocument,application/xaml+xml,application/vnd.ms-excel,application/vnd.ms-powerpoint,application/msword, */*
Accept-Language: zh-cn
User-Agent: Mozilla/4.0(compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; .NET CLR 2.0.50727; .NETCLR 3.0.4506.2152; .NET CLR 3.5.30729; InfoPath.1)
Accept-Encoding: gzip, deflate
Host: localhost:8080
Connection: Keep-Alive
注意:在请求头和请求实体中间有一个空行,由于本例是GET,故没有请求实体。
1.1.2HTTP响应
和HTTP请求对应,响应分为:
A. 响应——状态码——描述
B. 响应头
C. 响应实体
下面是上图的响应内容:
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Type: application/xml
Content-Length: 250
Date: Mon, 04 Jun 2012 07:25:47GMT
<?xml version="1.0"encoding="UTF-8"standalone="yes"?><ServerGroupList><ServerGroupname="UML_Z" groupId="19" description="UMLCluster"/><ServerGroup name="groupName122" groupId="20"/><ServerGroupname="serverGroup1" groupId="21"/></ServerGroupList>
1.2Socket与ServerSocket
1.2.1Socket
套接字是网络连接的端点。它使得应用程序可以从网络中读取数据,也可以向网络中写入数据。不同计算机上的两个应用程序可以通过套接字来完成相互交互通信。在java中套接字使用java.net.Socket表示。
1.2.2ServerSocket
Socket类表示一个客户端套接字,即,当想要连接到远程服务器应用程序是创建的套接字。但是如果你想要实现一个服务器应用程序(在这里我们想要实
现一个简单的web服务器),那么需要使用java.net.ServerSocket类,这是服务器套接字的实现。
1.2.3简单示例
(编写一个小程序:向本机8888端口发送一个字符串,然后接受并显示)
示例代码如下:
SendMessage.java
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
public class SendMessage {
public static void main(String[]args) {
try {
Socketsocket=new socket(InetAddress.getByName("127.0.0.1"),
8888);
OutputStreamoutput=socket.getOutputStream();
output.write("Helloworld!".getBytes());
output.close();
System.out.println("sendover");
} catch(UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
ReceiveMessage.java
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
public class ReceiveMessage {
public static void main(String[]args) {
Socketsocket=null;
try {
ServerSocketserverSocket=new ServerSocket(8888,
1, InetAddress.getByName("127.0.0.1"));
socket=serverSocket.accept();
//获取输入流
InputStreaminput=socket.getInputStream();
byte[] b=newbyte[2048];
StringBuffermessage=new StringBuffer(2048);
try {
int i=input.read(b);
for(int j=0;j<i;j++){
message.append((char)b[j]);
}
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("messageis:"+message.toString());
} catch(UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
1.3一个简单的web服务器
本章的应用程序放在ex01.pyromnt包下,包括三个类:
a.HttpServer
b.HttpRequest
c.HttpResponse
1.3.1HttpServer类
本章应用程序位于ex01.pyrmont包下,包括三个类
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
public class HttpServer {
public static final String WEB_ROOT = System.getProperty("user.dir")
+File.separator+"webroot";
private static booleanshutdown=false;
public static void main(String[]args){
newHttpServer().await();
}
privatevoid await(){
int port=7890;
ServerSocketserverSocket=null;
try{
serverSocket=newServerSocket(port,1,
InetAddress.getByName("127.0.0.1"));
}catch(IOException e){
e.printStackTrace();
System.exit(1);
}
try {
Socketsocket=null;
InputStreaminput=null;
OutputStreamoutput=null;
while(!shutdown){
socket=serverSocket.accept();
input=socket.getInputStream();
output=socket.getOutputStream();
HttpRequestrequest=new HttpRequest(input);
request.parse();
HttpResponseresponse=new HttpResponse(output);
response.setRequest(request);
response.sendStaticResource();
socket.close();
shutdown=request.getUri().equals("shutdown");
}
} catch(UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
在这里,我们简单的看看HttpServer中的await方法:
它首先实例化了一个ServerSocket对象实例(如下),
serverSocket=new ServerSocket(port, 1,InetAddress.getByName("127.0.0.1"));该对象侦听本地主机的8765端口,其backlog值为1;
随后进入了一个while循环,当serverSocket从8765端口收到HTTP请求后,serSocket对象的accept()方法返回一个Socket对象,等待结束:
socke=serverSocket.accept();
接受到HTTP请求之后,await方法从socket对象中获取输入流和输出流:
input=socket.getInputStream();
output=socket.getOutputStream();
之后,创建了HttpRequest对象,并调用其parse()方法解析HTTP请求的原始数据:
HttpRequest request=new HttpRequest(input);
request.parse();
然后,await()方法会创建一个HttpResponse对象来响应客户端:
HttpResponse response=newHttpResponse(output);
response.setRequest(request);
response.sendStaticResource();
最后,await()关闭套接字,调用Request类的getUri()方法来测试HTTP请求的URL是否是关闭命令,若是则程序退出while循环:
socket.close();
shutdown=request.getUri().equals(SHUTDOWN_COMMAND);
1.3.2HttpRequest类
import java.io.IOException;
import java.io.InputStream;
public class HttpRequest {
private InputStream input;
private String uri;
public String getUri() {
returnuri;
}
publicvoid setUri(String uri){
this.uri = uri;
}
public InputStreamgetInput() {
returninput;
}
publicvoidsetInput(InputStream input) {
this.input = input;
}
public HttpRequest(){
}
publicHttpRequest(InputStream input){
this.input=input;
}
//解析HTTP请求的原始数据
publicvoid parse(){
byte[] b=newbyte[2048];
StringBufferrequest=new StringBuffer(2048);
try {
int i=input.read(b);
for(int j=0;j<i;j++){
request.append((char)b[j]);
}
} catch (IOException e) {
e.printStackTrace();
}
this.uri=parseUri(request.toString());
}
//解析HTTP请求的URL
private StringparseUri(String requestString){
int index1,index2;
index1=requestString.indexOf(' ');
if(index1!=-1){
index2=requestString.indexOf(' ', index1+1);
if(index2>index1){
returnrequestString.substring(index1+1, index2);
}
}
returnnull;
}
}
说明:parse()方法是public的,它会从InputStream中读取整个字节流,并将字节数组存储到缓冲区中,然后它用该数组填充StringBuffer,最后调用private的parseUri(StringrequestString)来解析URL。
假设我们请求的URL是:http://localhost:7890/index.html
那么:
1.3.3HttpResponse类
一个HttpResponse对象表示一个Http响应,代码如下:
import java.io.File;
import java.io.FileInputStream;
import java.io.OutputStream;
public class HttpResponse {
private static final intBUFFER_SIZE=1024;
HttpRequest request;
OutputStream output;
public HttpResponse(){
}
publicHttpResponse(OutputStream output){
this.output=output;
}
public HttpRequestgetRequest() {
returnrequest;
}
publicvoidsetRequest(HttpRequest request) {
this.request = request;
}
public OutputStreamgetOutput() {
returnoutput;
}
publicvoid setOutput(OutputStreamoutput) {
this.output = output;
}
publicvoidsendStaticResource(){
byte[] bytes=newbyte[BUFFER_SIZE];
FileInputStreamfis=null;
File file=new File(HttpServer.WEB_ROOT,request.getUri());
try {
if(file.exists()){
fis=newFileInputStream(file);
intch=fis.read(bytes,0, BUFFER_SIZE);
while(ch!=-1){
output.write(bytes, 0,ch);
ch=fis.read(bytes,0,BUFFER_SIZE);
}
}
else{
output.write("file notfound".getBytes());
}
} catch (Exception e) {
e.printStackTrace();
}
finally{
if(fis!=null){
try{
fis.close();
}catch(Exception e){
e.printStackTrace();
}
}
}
}
}
说明:HTTP是基于请求/响应范式的,故在HttpResponse中首先需要获取HttpRequest中的uri,即,HttpResponse必须知道你(客户端)想要什么,这里还以http://localhost:7890/index.html为例,客户端请求的页面是index.html,然后将父路径和子路径传到java.IO.File类的构造函数中去构造File对象(此例中File是index.html),之后判断该文件是否存在,若不存在,则输出“file not found”到浏览器;否则使用OutputStream对象的write()方法将index.html输出到浏览器。
1.3.4运行本章程序
在编译java类之后,测试的url可以参考前文。效果如下图:
本章中我们可以请求静态资源,而不仅仅是html文件,比如图片: