1 引言
经过了一周的ruby学习后,为了让ruby基础得到巩固,我用socket写了一个处理http请求的gem包,我也不知道是否已经有大神完成类似功能的gem包,也不清楚我写的这个服务有什么实际的作用,这个项目也只是作为练手。
首先要吐槽一下的是,这个项目虽然代码量不是很多,但是对一个ruby初学者来说,全程用sublime来写还是蛮累的,建议使用RubyMine。
用sublime写代码总结的坑如下:
- 使用sublime编写完代码在irb上执行查看结果,若.rb文件未require使用到的类,不提示错误。只有首个rb文件未require会有对应的报错信息,如果是rb文件再引入第二个rb文件,而第二个rb文件没有require对应的类,没有对应的报错信息,坑啊。
- rb文件(非首个rb文件)中类名写错,rescue后报错信息在irb上输出一堆根本定义不到错误内容的信息,甚至错误发生在第几行也是定位不到。我就试过把YNHttp写成YnHttp,结果我花了很长时间在找bug,各种puts信息,血与泪的痛啊。T_T
- 最坑的莫过于sublime不能debug,所以每一次找bug,都要花费我很长时间,不断的做重复工作。
- 执行rb文件也麻烦,每一次都要到终端上cd到要执行的rb文件路径,再进入irb,再load要执行的rb文件。
本次源代码全部已放在我的Github上,路径:https://github.com/mia2002/yn_server
2 设计思想
我用笔粗略的画了整个逻辑执行过程,如下图所示:
首先,在接收Http请求和处理Http请求应在不同的线程中进行,线程1(接收请求)只负责接收Http请求,并把Http请求存放在队列中,线程2(处理Http请求)负责从队列中拿出请求,并对Http协议进行分析,提取路径和参数,分配到各自的task方法。其实这里就是使用到了生产者消费者模式。
本项目只使用了两条线程,其实更优的处理应该是处理Http请求的线程应根据服务器实际情况分配足够的线程数,并使用线程池管理所有线程,但是本次并没有对此进行优化。
3 HTTP协议
这里给网络知识忘记了或者压根就不知道的朋友稍微温习一下:
HTTP是Web浏览器和Web服务器之间通信的标准协议。HTTP指定客户端与服务器如何建立连接、客户端如何从服务器请求数据,服务器如何响应请求,以及最后如何关闭连接。
每个请求和响应都有同样的基本形式:一个首行、一个包含元数据的HTTP首部,然后是一个消息体。
GET请求:
GET /RubyServer/hello?name=yanyan&pwd=123 HTTP/1.1
Host: localhost:9000
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3)
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
Connection: keep-alive
像这样的GET请求不包含消息体,所以请求以一个空行结束。
第一行称为请求行,包括一个方法(GET/POST)、资源路径以及HTTP版本。
POST请求
POST /RubyServer/json HTTP/1.1
Host: localhost:3000
Content-Type: application/x-www-form-urlencoded
Connection: keep-alive
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3)
Content-Length: 21
name=yan&password=123
在POST请求头中空行后接着是请求主体,Content-Length指明消息体有多少个字节。在处理post请求中一般是根据这个参数来提取主体内容。
那么综上,本次代码就是实现去分析Http请求头,通过第一行能判断我们的请求到底是GET还是POST,如果是GET请求的话,参数是放在资源路径?后面;如果是POST请求,则读取Content-Length的值,并通过该值提取主体内容。
当然,HTTP请求并不只有GET和POST请求,这里我们只处理这两种情况,有兴趣的童鞋可以去拓展一下。
4 代码分析
经过简单的HTTP知识温习后,相信接下来的代码分析应该很好理解。
1.yn_socket_queue.rb
这个类目前的设计只是简单封装了一下,为以后的功能拓展做准备。
class YNSocketQueue
def initialize()
@queue=Queue.new
end
def push(socket)
raise "Illegal Argument, must be a TCPSocket Object!!" unless socket.is_a? TCPSocket
if socket != nil
@queue << socket
end
end
def take
@queue.pop
end
end
2.yn_request.rb
此类实现将获取到的请求参数由"name=yan&password=123"转换为Hash,我也不清楚ruby的API到底有没有这个功能的实现,我是找遍了String类、Array类和Hash类,都没发现有类似的功能,于是就自己实现。
通过Request.new.get(key)
可以实现根据参数名称获取参数的值
class YNRequest
include Enumerable
attr_reader :hash
def initialize(content="")
@hash = Hash.new
if content != ""
_arr = []
_arr = content.include?("&") ? content.split("&") : _arr.push(content)
_arr.each do |e|
__arr = e.split("=")
@hash[__arr[0].to_sym]=__arr[1]
end
end
end
def get(key)
@hash[key.to_sym]
end
def each
raise 'please provide a block!' unless block_given?
@hash.each do |e|
yield e
end
end
end
3.yn_http.rb
此类的设计主要是用来返回Http响应文。
class YNHttp
def initialize(_status=200,_server="Apache-Coyote/1.1",_pragma="no-cache",_control="no-cache",_content_type="text/json",_charset="UTF-8",_body="")
@status = _status
@server = _server
@pragma = _pragma
@cache_control = _control
@content_type = _content_type
@charset = _charset
@body = _body
@content_length = _body.length
end
@@status_hash={
100 => "CONTINUE",
200 => "OK",
201 => "CREATED",
202 => "ACCEPTED",
204 => "NO CONTENT",
302 => "MOVED TEMPORARILY",
400 => "BAD REQUEST",
401 => "UNAUTHORIZED",
402 => "PAYMENT REQUIRED",
403 => "FORBIDDEN",
404 => "NOT FOUND",
408 => "REQUEST TIMEOUT",
409 => "CONFLICT",
410 => "GONE",
500 => "INTERNAL SERVER ERROR"
}
def status=(_status)
@status=_status
end
def server=(_server)
@server=_server
end
def pragma=(_pragma)
@pragma=_pragma
end
def cache_control=(_control)
@cache_control=_control
end
def content_type=(_content_type)
@content_type=_content_type
end
def charset=(_charset)
@charset=_charset
end
def body=(result)
@content_length=result.size
@body=result
end
def response
"HTTP/1.1 #{@status} #{@@status_hash[@status]}\r\n" +
"Server:#{@server}\r\n" +
"Pragma:#{@pragma}\r\n" +
"Cache-Control:#{@cache_control}\r\n" +
"Content-Type:#{@content_type};charset=#{@charset}\r\n" +
"Content-Length:#{@content_length}\r\n" +
"\r\n" +
"#{@body}"
end
end
4.yn_route_util.rb
这个工具类主要是用来配置请求的资源路径对应Task中的方法名,HandlerRequest根据拿到的方法名动态执行Task中的方法。一开始我是考虑使用责任链来实现的,后面发现ruby有send这个方法可以动态执行类定义的方法,果断使用send来实现。
另外,本来原先设计是准备使用properties文件的,但是用gemspec打包后,不知道打包的properties文件到底被存放在什么路径下,试了好几个路径,都是提示file not found,知道怎么回事的大神们可以在评论或者发邮件告诉我,感激不尽!
# require 'yaml'
class YNRouteUtil
include Enumerable
@@route_hash = {
"/RubyServer/hello" => "say_hello",
"/RubyServer/json" => "test_json",
}
# def initialize
# @route_hash = Hash.new
# puts "route util initialize"
# _arr = YAML.load_file('route.properties').split(" ")
# puts "route_file: #{_arr}"
# _arr.each do |e|
# __arr = e.split("=")
# @route_hash[__arr[0]] = __arr[1]
# end
# end
def get_method(route)
@@route_hash[route]
end
def each
raise 'please provide a block!' unless block_given?
@@route_hash.each do |e|
yield e
end
end
end
5.yn_task.rb
这个类主要作用是用来处理请求的,本示例代码只提供了两个测试方法和一个默认方法,分别是say_hello(请求路径:/RubyServer/hello将执行该方法)、test_json(请求路径:/RubyServer/json将执行该方法)和default(请求路径找不到时执行该方法)。
以下这些方法对应的请求路径均在YNRouteUtil这个类中进行配置请求路径以及对应的YNTask类中的方法名,并在YNTask中添加该方法,实现对请求的处理,通过@request.get(key)
可以实现根据接口定义的参数名称来获取参数的值,YNHttp.new.body="json string"
返回处理后的json数据,最后,切记一定要返回最终的请求响应文,如test_json最后的http.response
require 'json'
require 'yn_http'
require 'yn_request'
class YNTask
def initialize(request)
@request = request
end
def method_missing(method_name)
puts "#{method_name} not found in YNTask,please check yn_route_uril.rb"
default
end
# route: /RubyServer/hello
def say_hello
begin
http = YNHttp.new
http.content_type = "text/html"
_param = ""
@request.each do |e|
_param+="#{e[0]}=#{e[1]}<br>"
end
http.body = "<html><head><title>Hello to Ruby Server</title></html><body><h1>Hi,welcome to Yan's ruby server!</h1><p>the request param:<br>#{_param}</p></body></html>"
# return the response result
http.response
rescue Exception => e
puts e.send(:caller)
end
end
# route: /RubyServer/json
def test_json
http = YNHttp.new
http.content_type = "text/json"
result = JSON.generate(@request.hash)
http.body = result
http.response # 最后必须返回影响文
end
# url not found
def default
http = YNHttp.new
http.content_type = "text/html"
http.status = 404
http.body = "<html><head><title>Welcome</title></html><body><h1>Welcome to Yan's ruby server!</h1><p><h3>404 Not Found</h3></p></body></html>"
http.response # 最后必须返回影响文
end
end
6.yn_socket_server.rb
该类主要实现开启一个TCPServer服务,接收http请求,并将接收到的请求push到队列中。
require 'socket'
require 'yn_socket_queue'
require 'yn_handle_request'
class YNSocketServer
# 初始化
# port 端口号
def initialize(port,queue)
@port = port
@queue = queue
end
def start_server
begin
@server=TCPServer.open(@port)
puts "start successfully!!"
loop{
@client=@server.accept
@queue.push(@client)
}
rescue Exception => e
puts e.send(:caller)
end
end
end
7.yn_handle_request.rb
该类是做处理Http请求,是最最核心的类,也是业务逻辑最复杂的类。
它主要做的处理有如下:
- 分析HTTP请求头
- 根据资源路径查找YNTask中的方法名
- 动态执行YNTask类中的方法,获取最终的响应文
- 返回响应文给请求客户端
require 'yn_socket_queue'
require 'yn_request'
require 'yn_task'
require 'yn_route_util'
# 处理http请求
# create by yan
class YNHandleRequest
def initialize(socket_queue)
@socket_queue=socket_queue
end
def handle
loop do
begin
client = @socket_queue.take
puts "-----------------------------------"
_method,path = client.gets.split
puts "url: #{path}"
puts "method: #{_method}"
headers={}
while line = client.gets.split(' ',2)
break if line[0]==""
headers[line[0].chop] = line[1].strip
end
data = ""
servlet_url = ""
if _method.upcase == 'POST'
data = client.read(headers["Content-Length"].to_i)
servlet_url = path
elsif _method.upcase == 'GET'
if path.include? '?'
# 带参数
data = path[(path.index('?')+1)..path.length]
servlet_url = path[0...path.index('?')]
else
data = ""
servlet_url = path
end
end
request = YNRequest.new(data)
puts "parameter: #{request.hash}"
util = YNRouteUtil.new
route = util.get_method(servlet_url)
task = YNTask.new(request)
route = "default" if route == nil || route.empty?
puts "route: #{route}"
_result = task.send(route) #动态执行方法
client.write(_result)
rescue Exception => e
puts e.send(:caller)
ensure
client.close
end
end
end
end
最后,我们可以分别开启两条线程来执行代码,一条线程主要用来负责接收Http请求,另外一条线程用来处理Http请求。
5 GEM打包
1.创建与项目同名的.gemspec文件,本例为:yn_server.gemspec,其实不同名也可以,但是为了便于管理,推荐还是建同名文件会比较好。
在.gemspec文件中写入如下内容:
Gem::Specification.new do |s|
s.name = 'yn_server'
s.version = '0.1'
s.date = '2017-03-23'
s.summary = 'Ruby Server!'
s.description = 'A simple socket server gem'
s.authors = ['Yan Ng']
s.email = 'yan@yerl.cn'
s.files = %w(
lib/yn_handle_request.rb
lib/yn_http.rb
lib/yn_server.rb
lib/yn_request.rb
lib/yn_route_util.rb
lib/yn_socket_queue.rb
lib/yn_socket_server.rb
lib/yn_task.rb
)
s.homepage =
'http://rubygems.org/gems/yn_server'
s.license = 'MIT'
end
注意,s.name一定要是执行的第一个rb文件名称,在打包的rb文件一定要有和该名字一样的ruby文件,否则,最终安装后,require我们这个name,系统会报找不到文件。我就被坑过,最后还是谷歌出来的。
2.生成gem包
在终端执行gem build后会在当前路径生成gem包。
gem build yn_server.gemspec
3.install gem
生成的gem包可以实现在本地安装,使用gem install命令
sudo gem install yn_manage-0.1.gem
通过gem list命令可以查看是否已经安装成功。
4.irb导入自己的gem包
通过gem install后我们就不需要再向从前那样使用load .rb文件,可以直接使用require引入要使用的类。
示例代码:
# 'yn_server'为gemspec中的s.name所配置的名称
require 'yn_server'
5.发布gem包
我们可以把包发布到rubygems.org,这样其他人在引用我们的gem包时就可以直接使用gem install
来安装。
1)首先得先到https://rubygems.org注册帐号,并完成邮件激活帐号。
2)命令行push gem
gem push yn_server-0.1.gem
等几分钟后,可以在rubygems.org上搜索到刚push的gem包信息。
3)命令行gem install
接下来,其他人要使用这个gem包,就可以直接打开终端输入命令:
sudo gem install yn_server
4)执行gem包
使用YNServer.start(port)
启动服务,参数端口可不传,不带参数时默认端口号为2000。
打开浏览器输入地址:
http://localhost:2000/RubyServer/hello?name=yan&pwd=123
执行结果:
终端结果:
6 心得体会
虽说我对Ruby还有很多不懂的地方,但是作为一个新手来说,我坚持完成了一个简单的ruby项目编写,这当中学习到很多东西。
e-mail:yan@yerl.cn
blog:mia2002.cn