最近学习了下Java NIO,又看了网上一些大神的文章帖子,感觉鹈鹕盖顶,受益匪浅。模仿大神代码,实现了一个简单的服务器(浏览器做客户端),在这里和大家分享和学习,初来乍到,有什么不对的地方还往大家及时的指出,我好及时改正。下面还是直接一步一步,看代码实现吧。
服务器启动
服务器启动步骤总结如下几步:
- 打开ServerSocketChannel通道
- 绑定一个服务器,并设置通道为非阻塞(因为下面使用了Selector)
- 创建选择器对象,并把ServerSocketChannel通道注册到选择器上面,并设置“接收”这个感兴趣的事件
- 让服务器可以不断的去接收请求
- 调用Selector的select(),得到当前准备就绪的通道数,如果不等于0,说明当前有通道可以处理事件
- 调用Selector的selectedKeys()方法,获取所有任务对象的Set集合
-
通过迭代器逐个取出事件,并判断该事件感兴趣的事件做出处理
【这里只是接收请求,和读取请求信息并给出反馈,所有只做了接收和读取的处理,具体代码如下】
/**
* 服务器启动
*
* @param port 服务器端口号
*/
public void server(int port) {
try {
//打开ServerSocketChannel通道,并绑定一个服务器,设置为非堵塞
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(port));
serverSocketChannel.configureBlocking(false);
//创建选择器对象,并把服务器通道注册到选择器,和设置了“接收”这个感兴趣的事件
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
//该方法是一个堵塞方法,返回已经准备就绪的通道数
int select = selector.select();
if (select != 0) {
//对任务对象进行遍历,分别处理每个任务
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
//每个任务
SelectionKey selectionKey = iterator.next();
//持有任务去处理的帮助类
ServerHolder serverHolder = new ServerHolder();
if (selectionKey.isAcceptable()) {
//对连接任务处理
SocketChannel socketChannel = serverHolder.connectSocketChannel(selectionKey);
} else if (selectionKey.isReadable()) {
//获得到请求的request信息对象
Request request = serverHolder.parse(selectionKey);
//判断请求对象是否传过了是null,是null就不操作,
// 并从任务中删除掉这个请求,结束掉这次请求
if (null == request) {
selectionKey.cancel();
continue;
}
//根据request请求,给出相应的响应
serverHolder.couple(selectionKey, request);
}
//执行完,从任务集合中删除这个任务对象
iterator.remove();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
接收事件的处理
1.通过事件任务对象得到服务器对象,并接受请求
2.并把接受的这个请求(SocketChannel)注册到Selector上
3.并设置这个请求感兴趣的事件“读取“
【其实这理我是这样理解的:ServerSocketChannel相当于一个泳池,Selector相当远管理员,请求端相当于客户。泳池对游泳这件事情比较”感兴趣“,客户来游泳,找到了管理员,管理员查看泳池得到了一个没人的可以游泳的通道,管理员把客户领到了这条涌到上。代码如下】
/**
* 处理接收事件
*
* @param selectionKey 任务对象
* @return
*/
public SocketChannel connectSocketChannel(SelectionKey selectionKey) {
try {
ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel = channel.accept();
socketChannel.configureBlocking(false);
Selector selector = selectionKey.selector();
socketChannel.register(selector, SelectionKey.OP_READ);
return socketChannel;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
读取并解析
当连接接通之后,接下来就是读取客户端发过来的数据并解析出我们需要的内容。这里使用浏览器作为客户端,了解浏览器请求的人都知道,一个简单的请求包括四部分:请求行、请求头、空行 、请求体 。盗用图15-4:
如图所示:
1.请求行包含3部分,没部分之间都用空格分隔,并且占一行
2.请求头没一行对应一个请求头信息
3.空行,分割请求头和请求体
4.请求体内(服务器只做简单的GET请求,所以这里没有对请求体进行操作)
按照这样的格式:我们读取到客户端发送过来的请求信息,然后按照上面的格式在对其进行解析,从而得到Request对象。
读取请求信息并解析代码
/**
* 解析请求
*
* @param selectionKey
* @return
*/
public Request parse(SelectionKey selectionKey) {
//初始化Request对象
Request request = new Request(selectionKey);
try {
//用来拼接存储读取到的数据
StringBuilder stringBuilder = new StringBuilder();
SocketChannel channel = (SocketChannel) selectionKey.channel();
ByteBuffer allocate = ByteBuffer.allocate(1024);
int read = channel.read(allocate);
while (read > 0) {
allocate.flip();
stringBuilder.append(new String(allocate.array(), 0, read));
allocate.clear();
read = channel.read(allocate);
}
String string = stringBuilder.toString();
//如果没有读取到数据,就返回一个null
if (string.equals("") || null == string) {
return null;
}
//对得到的数据进行解析
String[] strings = string.split("\r\n");
//请求行
String httpRequestLine = strings[0];
String[] data = httpRequestLine.split(" ");
//System.out.println("data:" + httpRequestLine);
request.setMethod(data[0]);
request.setRequestURI(data[1]);
request.setVersion(data[2]);
//请求头
ArrayList<Header> list = new ArrayList<>();
for (int i = 1; i < strings.length; i++) {
String headerInfo = strings[i];
String[] item = headerInfo.split(": ");
Header header = new Header(item[0], item[1]);
list.add(header);
}
request.setHeaders(list);
return request;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
Request对象代码
public class Request {
private String method; // 请求方式
private String requestURI; // 请求的uri
private String version; //http协议版本
private List<Header> headers; // 封装请求头
private final SelectionKey selectionKey; // 用于获取通道
/*****************************************************************************************/
/*****************************************************************************************/
public Request(SelectionKey selectionKey) {
this.selectionKey = selectionKey;
}
public String getMethod() {
return method;
}
public Request setMethod(String method) {
this.method = method;
return this;
}
public String getRequestURI() {
return requestURI;
}
public Request setRequestURI(String requestURI) {
this.requestURI = requestURI;
return this;
}
public String getVersion() {
return version;
}
public Request setVersion(String version) {
this.version = version;
return this;
}
public List<Header> getHeaders() {
return headers;
}
public Request setHeaders(List<Header> headers) {
this.headers = headers;
return this;
}
public SelectionKey getSelectionKey() {
return selectionKey;
}
/*****************************************************************************************/
/*****************************************************************************************/
}
对请求做出响应
上面我获取到了请求的数据信息Requestrian,如果请求信息不为空,我们就根据请求做出响应,我们这里的反馈依据的请求的URL信息。同样,我们要封装好浏览器能解析的数据,也就是响应体(如下盗图),给出反馈。其他重要的数据有,请求行(协议版本、状态码、状态描述),还有Content-Type这个请求头(要反馈信息的格式),
做出响应并反馈代码
/**
* 响应并反馈
*
* @param selectionKey
* @param request
*/
public void couple(SelectionKey selectionKey, Request request) {
try {
//初始化Responce对象
Responce responce = new Responce(selectionKey, request);
//获取请求过来的URI
String requestURI = request.getRequestURI();
//URI带"/",跳过这条线
String substring = requestURI.substring(1);
File file = new File(substring);
boolean isfile = file.exists();
System.out.println(substring + ":" + isfile);
//文件是否存在
if (isfile) {
//判断URI是否访问的是一个文件夹
if (file.isDirectory()) {
responce.setStatus("202");
responce.setDes("Disagreeing to Requests");
responce.setContentType("text/html;charset=UTF-8");
} else {
responce.setStatus("200");
responce.setDes("OK");
//是一个文件,通过后缀名得到该文件的所属类型,只是部分常用文件类型
if (requestURI.endsWith("txt")) {
responce.setContentType("text/html;charset=UTF-8");
} else if (requestURI.endsWith("jpg")) {
responce.setContentType("image/jpeg");
} else if (requestURI.endsWith("png")) {
responce.setContentType("image/png");
} else if (requestURI.endsWith("gif")) {
responce.setContentType("image/gif");
}
}
} else {
//如果只是一个"/"说明访问的是服务器的默认端口号
if (requestURI.equals("/")) {
responce.setStatus("200");
responce.setDes("MAIN SERVER!");
} else {
//文件没有找到
responce.setStatus("404");
responce.setDes("Not File!");
}
responce.setContentType("text/html;charset=UTF-8");
}
/*************************************拼接响应信息*************************************/
//响应行
//String responseLine = this.version + " " + this.status + " " + this.des + "\r\n";
String responseLine = responce.getVersion() + " " + responce.getStatus() + " " + responce.getDes() + "\r\n";
StringBuilder stringBuilder = new StringBuilder(responseLine);
//响应头
for (Header header : responce.getHeaders()) {
stringBuilder.append(header.getName()).append(": ").append(header.getValue()).append("\r\n");
}
//空行
stringBuilder.append("\r\n");
//无响应体的响应信息
String responseHeader = stringBuilder.toString();
//响应
SocketChannel channel = (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffer = byteBuffer = ByteBuffer.wrap(responseHeader.getBytes("UTF-8"));
channel.write(byteBuffer);
/*************************************做出反馈*************************************/
if (isfile) {
if (file.isDirectory()) {
//文件夹
String content = "拒绝访问文件夹,喵喵喵…";
ByteBuffer buffer = ByteBuffer.wrap(content.getBytes("UTF-8"));
channel.write(buffer);
} else {
//文件
byte[] content = getContent(substring);
if (content != null) {
ByteBuffer buffer = ByteBuffer.wrap(content);
channel.write(buffer);
}
}
} else {
String content;
if (requestURI.equals("/")) {
content = "欢迎访问【大菊为重】服务器,喵喵喵…";
} else {
content = "没有找到你要的文件,喵喵喵喵…";
}
ByteBuffer buffer = ByteBuffer.wrap(content.getBytes("UTF-8"));
channel.write(buffer);
}
channel.close();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
获取文件的byte[]代码
/**
* 获取文件的byte[]数组
*
* @param dataOrigin
* @return
*/
public byte[] getContent(String dataOrigin) {
try {
RandomAccessFile randomAccessFile = new RandomAccessFile(dataOrigin, "rw");
FileChannel channel = randomAccessFile.getChannel();
//如果数据超过int最大值,就需要用流来传递了
ByteBuffer buffer = ByteBuffer.allocate((int) channel.size());
channel.read(buffer);
buffer.flip();
return buffer.array();
} catch (FileNotFoundException e) {
e.printStackTrace();
return null;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
Responce对象代码
public class Responce {
private final String version = "HTTP/1.1";
private String status;
private String des;
private String contentType;
private List<Header> headers = new ArrayList<>();
private final SelectionKey selectionKey;
private final Request Request;
public Responce(SelectionKey selectionKey, com.wlt.tomcat.modle.Request request) {
this.selectionKey = selectionKey;
this.Request = request;
}
/************************************************************/
/************************************************************/
public String getVersion() {
return version;
}
public String getStatus() {
return status;
}
public Responce setStatus(String status) {
this.status = status;
return this;
}
public String getDes() {
return des;
}
public Responce setDes(String des) {
this.des = des;
return this;
}
public String getContentType() {
return contentType;
}
public Responce setContentType(String contentType) {
this.contentType = contentType;
Header header = new Header();
header.setName("Content-Type");
header.setValue(contentType);
headers.add(header);
return this;
}
public List<Header> getHeaders() {
return headers;
}
public Responce setHeaders(List<Header> headers) {
this.headers = headers;
return this;
}
public SelectionKey getSelectionKey() {
return selectionKey;
}
public com.wlt.tomcat.modle.Request getRequest() {
return Request;
}
/************************************************************/
/************************************************************/
}
最后贴下代码的下载地址简单服务器代码
闻道有先后,术业有专攻。往大家看过后给出宝贵的意见,或不对的地方给与指出我并改正,并对这个小应用不断更新,尽可能的完善下,谢谢各位!