如何开发自己的HttpServer-NanoHttpd源码解读

如何开发自己的HttpServer-NanoHttpd源码解读,第1张

1.能接受HttpRequest并返回HttpResponse

2.满足一个Server的基本特征,能够长时间运行

关于Http协议一般HttpServer都会声明支持Http协议的哪些特性,nanohttpd作为一个轻量级的httpserver只实现了最简单、最常用的功能,不过我们依然可以从中学习很多。

首先看下NanoHttpd类的start函数

[java] view plaincopy

public void start() throws IOException {

myServerSocket = new ServerSocket()

myServerSocket.bind((hostname != null) ? new InetSocketAddress(hostname, myPort) : new InetSocketAddress(myPort))

myThread = new Thread(new Runnable() {

@Override

public void run() {

do {

try {

final Socket finalAccept = myServerSocket.accept()

registerConnection(finalAccept)

finalAccept.setSoTimeout(SOCKET_READ_TIMEOUT)

final InputStream inputStream = finalAccept.getInputStream()

asyncRunner.exec(new Runnable() {

@Override

public void run() {

OutputStream outputStream = null

try {

outputStream = finalAccept.getOutputStream()

TempFileManager tempFileManager = tempFileManagerFactory.create()

HTTPSession session = new HTTPSession(tempFileManager, inputStream, outputStream, finalAccept.getInetAddress())

while (!finalAccept.isClosed()) {

session.execute()

}

} catch (Exception e) {

// When the socket is closed by the client, we throw our own SocketException

// to break the "keep alive" loop above.

if (!(e instanceof SocketException &&"NanoHttpd Shutdown".equals(e.getMessage()))) {

e.printStackTrace()

}

} finally {

safeClose(outputStream)

safeClose(inputStream)

safeClose(finalAccept)

unRegisterConnection(finalAccept)

}

}

})

} catch (IOException e) {

}

} while (!myServerSocket.isClosed())

}

})

myThread.setDaemon(true)

myThread.setName("NanoHttpd Main Listener")

myThread.start()

}

1.创建ServerSocket,bind制定端口

2.创建主线程,主线程负责和client建立连接

3.建立连接后会生成一个runnable对象放入asyncRunner中,asyncRunner.exec会创建一个线程来处理新生成的连接。

4.新线程首先创建了一个HttpSession,然后while(true)的执行httpSession.exec。

这里介绍下HttpSession的概念,HttpSession是java里Session概念的实现,简单来说一个Session就是一次httpClient->httpServer的连接,当连接close后session就结束了,如果没结束则session会一直存在。这点从这里的代码也能看到:如果socket不close或者exec没有抛出异常(异常有可能是client段断开连接)session会一直执行exec方法。

一个HttpSession中存储了一次网络连接中server应该保存的信息,比如:URI,METHOD,PARAMS,HEADERS,COOKIES等。

5.这里accept一个client的socket就创建一个独立线程的server模型是ThreadServer模型,特点是一个connection就会创建一个thread,是比较简单、常见的socket server实现。缺点是在同时处理大量连接时线程切换需要消耗大量的资源,如果有兴趣可以了解更加高效的NIO实现方式。

当获得client的socket后自然要开始处理client发送的httprequest。

Http Request Header的parse:

[plain] view plaincopy

// Read the first 8192 bytes.

// The full header should fit in here.

// Apache's default header limit is 8KB.

// Do NOT assume that a single read will get the entire header at once!

byte[] buf = new byte[BUFSIZE]

splitbyte = 0

rlen = 0

{

int read = -1

try {

read = inputStream.read(buf, 0, BUFSIZE)

} catch (Exception e) {

safeClose(inputStream)

safeClose(outputStream)

throw new SocketException("NanoHttpd Shutdown")

}

if (read == -1) {

// socket was been closed

safeClose(inputStream)

safeClose(outputStream)

throw new SocketException("NanoHttpd Shutdown")

}

while (read >0) {

rlen += read

splitbyte = findHeaderEnd(buf, rlen)

if (splitbyte >0)

break

read = inputStream.read(buf, rlen, BUFSIZE - rlen)

}

}

1.读取socket数据流的前8192个字节,因为http协议中头部最长为8192

2.通过findHeaderEnd函数找到header数据的截止位置,并把位置保存到splitbyte内。

[java] view plaincopy

if (splitbyte <rlen) {

inputStream.unread(buf, splitbyte, rlen - splitbyte)

}

parms = new HashMap<String, String>()

if(null == headers) {

headers = new HashMap<String, String>()

}

1.http协议规定header和body之间使用两个回车换行分割

1.Http协议第一行是Method URI HTTP_VERSION

2.后面每行都是KEY:VALUE格式的header

3.uri需要经过URIDecode处理后才能使用

4.uri中如果包含?则表示有param,httprequest的param一般表现为:/index.jsp?username=xiaoming&id=2

下面是处理cookie,不过这里cookie的实现较为简单,所以跳过。之后是serve方法,serve方法提供了用户自己实现httpserver具体逻辑的很好接口。在NanoHttpd中的serve方法实现了一个默认的简单处理功能。

[java] view plaincopy

发送response的步骤如下:

1.设置mimeType和Time等内容。

2.创建一个PrintWriter,按照HTTP协议依次开始写入内容

3.第一行是HTTP的返回码

4.然后是content-Type

5.然后是Date时间

6.之后是其他的HTTP Header

7.设置Keep-Alive的Header,Keep-Alive是Http1.1的新特性,作用是让客户端和服务器端之间保持一个长链接。

8.如果客户端指定了ChunkedEncoding则分块发送response,Chunked Encoding是Http1.1的又一新特性。一般在response的body比较大的时候使用,server端会首先发送response的HEADER,然后分块发送response的body,每个分块都由chunk length\r\n和chunk data\r\n组成,最后由一个0\r\n结束。

9.如果没指定ChunkedEncoding则需要指定Content-Length来让客户端指定response的body的size,然后再一直写body直到写完为止。

初看OkHttp源码,由于对Address、Route、Proxy、ProxySelector、RouteSelector等理解不够,读源码非常吃力,看了几遍依然对于寻找复用连接、创建连接、连接服务器、连接代理服务器、创建隧道连接等逻辑似懂非懂,本篇决定梳理一遍相关的概念及基本原理。

● HTTP/1.1(HTTPS)

● HTTP/2

● SPDY

一个http请求的流程(直连):

1、输入url及参数;

2、如果是url是域名则解析ip地址,可能对应多个ip,如果没有指定端口,则用默认端口,http请求用80;

3、创建socket,根据ip和端口连接服务器(socket内部会完成3次TCP握手);

4、socket成功连接后,发送http报文数据。

一个https请求的流程(直连):

1、输入url及参数;

2、如果是url是域名则解析ip地址,可能对应多个ip,如果没有指定端口,则用默认端口,https请求用443;

3、创建socket,根据ip和端口连接服务器(socket内部会完成3次TCP握手);

4、socket成功连接后进行TLS握手,可通过java标准款提供的SSLSocket完成;

5、握手成功后,发送https报文数据。

1、分类

● HTTP代理:普通代理、隧道代理

● SOCKS代理:SOCKS4、SOCKS5

2、HTTP代理分类及说明

普通代理

HTTP/1.1 协议的第一部分。其代理过程为:

● client 请求 proxy

● proxy 解析请求获取 origin server 地址

● proxy 向 origin server 转发请求

● proxy 接收 origin server 的响应

● proxy 向 client 转发响应

其中proxy获取目的服务器地址的标准方法是解析 request line 里的 request-URL。因为proxy需要解析报文,因此普通代理无法适用于https,因为报文都是加密的。

隧道代理

通过 Web 代理服务器用隧道方式传输基于 TCP 的协议。

请求包括两个阶段,一是连接(隧道)建立阶段,二是数据通信(请求响应)阶段,数据通信是基于 TCP packet ,代理服务器不会对请求及响应的报文作任何的处理,都是原封不动的转发,因此可以代理 HTTPS请求和响应。

代理过程为:

● client 向 proxy 发送 CONNET 请求(包含了 origin server 的地址)

● proxy 与 origin server 建立 TCP 连接

● proxy 向 client 发送响应

● client 向 proxy 发送请求,proxy 原封不动向 origin server 转发请求,请求数据不做任何封装,为原生 TCP packet.

3、SOCKS代理分类及说明

● SOCKS4:只支持TCP协议(即传输控制协议)

● SOCKS5: 既支持TCP协议又支持UDP协议(即用户数据包协议),还支持各种身份验证机制、服务器端域名解析等。

SOCK4能做到的SOCKS5都可得到,但反过来却不行,比如我们常用的聊天工具QQ在使用代理时就要求用SOCKS5代理,因为它需要使用UDP协议来传输数据。

有了上面的基础知识,下面分析结合源码分析OkHttp路由相关的逻辑。OkHttp用Address来描述与目标服务器建立连接的配置信息,但请求输入的可能是域名,一个域名可能对于多个ip,真正建立连接是其中一个ip,另外,如果设置了代理,客户端是与代理服务器建立直接连接,而不是目标服务器,代理又可能是域名,可能对应多个ip。因此,这里用Route来描述最终选择的路由,即客户端与哪个ip建立连接,是代理还是直连。下面对比下Address及Route的属性,及路由选择器RouteSelector。

描述与目标服务器建立连接所需要的配置信息,包括目标主机名、端口、dns,SocketFactory,如果是https请求,包括TLS相关的SSLSocketFactory 、HostnameVerifier 、CertificatePinner,代理服务器信息Proxy 、ProxySelector 。

Route提供了真正连接服务器所需要的动态信息,明确需要连接的服务器IP地址及代理服务器,一个Address可能会有很多个路由Route供选择(一个DNS对应对个IP)。

Address和Route都是数据对象,没有提供操作方法,OkHttp另外定义了RouteSelector来完成选择的路由的操作。

1、读取代理配置信息:resetNextProxy()

读取代理配置:

● 如果有指定代理(不读取系统配置,在OkHttpClient实例中指定),则只用1个该指定代理;

● 如果没有指定,则读取系统配置的,可能有多个。

2、获取需要尝试的socket地址(目标服务器或者代理服务器):resetNextInetSocketAddress()

结合Address的host和代理,解析要尝试的套接字地址(ip+端口)列表:

● 直连或者SOCK代理, 则用目标服务器的主机名和端口,如果是HTTP代理,则用代理服务器的主机名和端口;

● 如果是SOCK代理,根据目标服务器主机名和端口号创建未解析的套接字地址,列表只有1个地址;

● 如果是直连或HTTP代理,先DNS解析,得到InetAddress列表(没有端口),再创建InetSocketAddress列表(带上端口),InetSocketAddress与InetAddress的区别是前者带端口信息。

3、获取路由列表:next()

选择路由的流程解析:

● 遍历每个代理对象,可能多个,直连的代理对象为Proxy.DIRECT(实际是没有中间代理的);

● 对每个代理获取套接字地址列表;

● 遍历地址列表,创建Route,判断Route如果在路由黑名单中,则添加到失败路由列表,不在黑名单中则添加到待返回的Route列表;

● 如果最后待返回的Route列表为空,即可能所有路由都在黑名单中,实在没有新路由了,则将失败的路由集合返回;

● 传入Route列表创建Selection对象,对象比较简单,就是一个目标路由集合,及读取方法。

为了避免不必要的尝试,OkHttp会把连接失败的路由加入到黑名单中,由RouteDatabase管理,该类比较简单,就是一个失败路由集合。

1、创建Address

Address的创建在RetryAndFollowUpInteceptor里,每次请求会声明一个新的Address及StreamAllocation对象,而StreamAllocation使用Address创建RouteSelector对象,在连接时RouteSelector确定请求的路由。

每个Requst都会构造一个Address对象,构造好了Address对象只是有了与服务器连接的配置信息,但没有确定最终服务器的ip,也没有确定连接的路由。

2、创建RouteSelector

在StreamAllocation声明的同时会声明路由选择器RouteSelector,为一次请求寻找路由。

3、选择可用的路由Route

下面在测试过程跟踪实例对象来理解,分别测试直连和HTTP代理HTTP2请求路由的选择过程:

● 直连请求流程

● HTTP代理HTTPS流程

请求url: https://www.jianshu.com/p/63ba15d8877a

1、构造address对象

2、读取代理配置:resetNextProxy

3、解析目标服务器套接字地址:resetNextInetSocketAddress

4、选择Route创建RealConnection

5、确定协议

测试方法:

● 在PC端打开Charles,设置端口,如何设置代理,网上有教程,比较简单;

● 手机打开WIFI,选择连接的WIFI修改网络,在高级选项中设置中指定了代理服务器,ip为PC的ip,端口是Charles刚设置的端口;

● OkHttpClient不指定代理,发起请求。

1、构造address对象

2、读取代理配置:resetNextProxy

3、解析目标服务器套接字地址:resetNextInetSocketAddress

4、选择Route创建RealConnection

5、创建隧道

由于是代理https请求,需要用到隧道代理。

从图可以看出,建立隧道其实是发送CONNECT请求,header包括字段Proxy-Connection,目标主机名,请求内容类似:

6、确定协议,SSL握手

1、代理可分为HTTP代理和SOCK代理;

2、HTTP代理又分为普通代理和隧道代理;普通代理适合明文传输,即http请求;隧道代理仅转发TCP包,适合加密传输,即https/http2

3、SOCK代理又分为SOCK4和SOCK5,区别是后者支持UDP传输,适合代理聊天工具如QQ;

4、没有设置代理(OkHttpClient没有指定同时系统也没有设置),客户端直接与目标服务器建立TCP连接;

5、设置了代理,代理http请求时,客户端与代理服务器建立TCP连接,如果代理服务器是域名,则解释代理服务器域名,而目标服务器的域名由代理服务器解析;

6、设置了代理,代理https/http2请求时,客户端与代理服务器建立TCP连接,发送CONNECT请求与代理服务器建立隧道,并进行SSL握手,代理服务器不解析数据,仅转发TCP数据包。

如何正确使用 HTTP proxy

OkHttp3中的代理与路由

HTTP 代理原理及实现(一)

结论:①string URi = "http://192.168.0.105//www/"

URi的值应该是一个URL地址吧,怎么105后面有两个/

②你这样的访问方式涉及到客户端对服务器端的访问权限问题,我没试过以这样的方式将文件上传到服务器的,而且我觉得应该不是这样的方式。

解决方案建议:

①可以在服务器端开通ftp功能,将文件发送到ftp里,代码如下:

privatevoidbutton1_Click(objectsender,EventArgse)

{

WebClientw=newWebClient()

w.Credentials=newNetworkCredential("sa","sa")//登陆ftp的用户名密码

w.UploadFile("ftp://221.224.78.82/skdb/up.xls",@"e:\1.xls")////前面是远程ftp文件夹路径后面是:本地上传的文件路径

w.Dispose()

MessageBox.Show("上传成功!")

}

②服务器端有IIS的话通过WebService的方式实现文件上传

这个方法相对比较复杂,建议用第一种,如果要采用这种的话到时我再发一个例子给你⊙﹏⊙


欢迎分享,转载请注明来源:夏雨云

原文地址:https://www.xiayuyun.com/zonghe/200137.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2023-04-02
下一篇2023-04-02

发表评论

登录后才能评论

评论列表(0条)

    保存