Tomcat 学习进阶历程之Socket
在前一篇了又重新学习了一下HTTP协议,对它的工作过程和原理又加深了认识。那么当我们通过浏览器访问一个在线资源的时候,浏览器是怎么要将我们的请求发送到资源所在的服务器,又如何获得服务器对请求的响应呢。其实它用的就是我们常见的Socket。
Socket中文通常翻译为‘套接字’,套接字是两台机器之间的通信端点,Socket是工作在TCP/IP协议之上的。两台机器通过Socket通信时有两种方式,一种是无差错的TCP方式,一种是无序有且可以有差错的UDP方式。
TCP方式一般用在文件传输中,因为两个机器在通信前一般要经过三次握手的连接,所以开销也比较大。
UDP是一种可以有差错的协议,它发送的数据是无序的,一般用在视频,通话等场景下。这些场景一般不要求数据的完整性,可以容许部分的错误,只要数据在一定程度上是连续的即可。
1、 UDP方式
DatagramPacket表示数据报包,数据报包用来实现无连接包投递服务。每条报文仅根据该包中包含的信息从一台机器路由到另一台机器。从一台机器发送到另一台机器的多个包可能选择不同的路由,也可能按不同的顺序到达。不对包投递做出保证。
DatagramSocket表示用来发送和接收数据报包的套接字。数据报套接字是包投递服务的发送或接收点。每个在数据报套接字上发送或接收的包都是单独编址和路由的。从一台机器发送到另一台机器的多个包可能选择不同的路由,也可能按不同的顺序到达。在 DatagramSocket 上总是启用 UDP 广播发送。
2、 TCP方式
在JAVA中,通过Socket以TCP方式发送与接收数据,常用的JAVAAPI是Socket与ServerSocket。其中ServerSocket类实现服务器套接字。服务器套接字等待请求通过网络传入。它基于该请求执行某些操作,然后可能向请求者返回结果。
下面通过两段代码,看一个单线程的客户端向服务端的Socket连接
1)服务端代码public static void main(String[] args) {
// TODO Auto-generated method stub
try {
//建立一个服务端Socket,在1355端口等接收数据。默认是本机
ServerSocket server = new ServerSocket(1355);
//accept方法会在指定端口一直侦听,在有链接到来之前会一直阻塞
Socket socket = server.accept();
//有链接到来时,获得SOCKET的输入流,用来获取客户端发送过来的数据
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()) );
//获取Socket的输出流,用来向客户端发送数据
PrintWriter out = new PrintWriter(socket.getOutputStream());
while(true){
String line;
//readLine是一个阻塞方法,在输入数据可用、检测到流的末尾或者抛出异常前,此方法一直阻塞
line = br.readLine();
if(line !=null){
//向客户端响应数据
out.println("server received "+line);
out.flush();
//如果客户端输出的是bye,关闭链接
if(line.equals("bye"))
break;
}
}
socket.close();
// br.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
2)客户端代码
public static void main(String[] args) {
// TODO Auto-generated method stub
try {
//向本机的1355端口请求链接,请求连接时要求服务端已经启动,否则抛出异常
Socket socket = new Socket("127.0.0.1",1355);
//有链接到来时,获得SOCKET的输入流,用来获取服务端发送过来的数据
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//获取Socket的输出流,用来向服务端发送数据
PrintWriter out = new PrintWriter(socket.getOutputStream());
//用来接收用户控制台的输入
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
while(true){
//readLine是一个阻塞方法,在输入数据可用、检测到流的末尾或者抛出异常前,此方法一直阻塞
String msg = reader.readLine();
out.flush();
if(msg.equals("bye")){
break;
}
//打印服务端返回的数据
System.out.println(in.readLine());
}
socket.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
测试时先启动服务端,再启动客户端,就可以通过客户端的控制台向服务器发送数据,并接收客户端的响应信息。
通过上面的例子,我们可以总结以下几点:
1) 服务器先启动,并等待客户端的连接请求
2) 客户端在需要时(可以是任务时候),向服务端发起连接请求
3) 服务端接收到客户发起的连接请求,接收客户端发送过的数据,可以根据需要对数据进行处理,然后向客户端发送响应数据。
4) 服务端并不主动发起连接通讯。
通过上面几点我们可以发现,这种工作方式正是J2EE中常用的工作方式。我们通过浏览器向一个站点发起请求时,Web服务器就相当于服务端的ServerSocket,浏览器相当于客户端的Socket,Web服务器接收客户浏览器的请求,经过对请求处理后向浏览器发送响应数据,然后浏览器将响应数据显示出来。
下面,通过几小段代码,使用Socket手动建立一个Web服务器,响应客户端浏览器对服务器静态资源的请求。下面的代码需要一点点HTTP协议的知识。
为了测试以下的代码,可以新建一个项目,JAVA项目或Web项目都可以httpServer代码如下
public class HttpServer {
public static final String WEB_ROOT = System.getProperty("user.dir")+File.separator+"webroot";
private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";
private boolean shutdown = false;
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
HttpServer server = new HttpServer();
server.await();
}
public void await(){
ServerSocket serverSocket = null;
int port = 8080;
try{
//在本机8080端口开启一个服务端SOCKET
serverSocket =
new ServerSocket(port,1,InetAddress.getByName("127.0.0.1"));
}catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
System.exit(1);
}
//如果传递的请求不是shutdown,调用accept,等待客户端浏览器的请求
while(!shutdown){
Socket socket = null;
InputStream input = null;
OutputStream output = null;
try{
socket = serverSocket.accept();
//接收到请求,获取输入输出流
input = socket.getInputStream();
output = socket.getOutputStream();
Request request = new Request(input);
//格式化请求,解析HTTP请求头信息,解析出请求URI
request.parse();
//向客户端发送响应信息,然后关闭SOCKET
Response response = new Response(output);
response.setRequest(request);
response.sendStaticResource();
socket.close();
shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
}catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
continue;
}
}
}
}
Request代码如下:
public class Request {
public Request(InputStream input) {
// TODO Auto-generated constructor stub
this.input = input;
}
private InputStream input;
private String uri;
/**
* 返回HTTP请求字符串中头两个空格之间的文本。
* HTTP请求头一行是‘请求行’格式如:GET /index.html HTTP/1.1
* 所以此方法返回的是请求的URI
*/
private String parseUri(String requestString){
int index1,index2;
index1 = requestString.indexOf(" ");
if(index1 != -1){
index2 = requestString.indexOf(" ",index1+1);
if(index2 > index1)
return requestString.substring(index1+1,index2);
}
return null;
}
/**
* 从Socket的输入流中获取请求信息
*/
public void parse(){
StringBuffer request = new StringBuffer();
int i;
byte[] buffer = new byte[2048];
try{
i = input.read(buffer);
}catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
i = -1;
}
for(int j=0;j<i;j++){
request.append((char)buffer[j]);
}
uri = parseUri(request.toString());
}
public String getUri() {
return uri;
}
public void setUri(String uri) {
this.uri = uri;
}
}
Response代码如下:
public class Response {
private static final int BUFFER_SIZE = 1024;
Request request;
OutputStream output;
public Response(OutputStream output){
this.output = output;
}
public void setRequest(Request request){
this.request = request;
}
/**
* 根据客户端浏览器的请求,读取一个静态资源文件响应到客户端
*/
public void sendStaticResource()throws IOException{
byte[] bytes = new byte[BUFFER_SIZE];
FileInputStream fis = null;
try{
//根据请求URI,读取指定目录下的静态资源文件
File file = new File(HttpServer.WEB_ROOT,request.getUri());
if(file.exists()){
fis = new FileInputStream(file);
int ch = fis.read(bytes,0,BUFFER_SIZE);
while(ch!=-1){
output.write(bytes,0,ch);
ch = fis.read(bytes,0,BUFFER_SIZE);
}
}else{
//资源文件不存在就返回一个错误页面
StringBuffer sb = new StringBuffer();
sb.append("HTTP/1.1 404 FILE NOT FOUND\r\n");
sb.append("Content-Type: text/html\r\n");
sb.append("Content-Length: 23\r\n");
sb.append("\r\n");
sb.append("<h1>File Not Found</h1>");
output.write(sb.toString().getBytes());
}
}catch (Exception e) {
// TODO: handle exception
System.out.println(e.toString());
}finally{
if(fis!=null)
fis.close();
}
}
}
为了测试,还建立了一个名为 hello.html 的简单 HTML 页面。内容如下:
<html>
<body>
<h4>大家好,欢迎访问我!!!</h4>
</body>
</html>
测试时,先启动HttpServer类。然后在浏览器地址栏中输出: http://localhost:8080/aa.html,因为aa.html页面并不存在,所以可在浏览器中看到如下的响应:
然后再输入http://localhost:8080/hello.html测试,如下:
可以看到成功访问了hello.html这个静态资源文件。
通过以上的内容大家可以看到Socket的基本工作原理。也可以看到Web服务器一般是如何工作的。