使用ranch tcp开发服务端

Ranch:

简单来说,Ranch就是一个tcp acceptor pool,用于高并发下的tcp连接建立与管理。可以设置并发链接最大数量,在不关闭socket连接的情况下可以动态升级连接池。Cowboy就是使用的ranch。

https://github.com/ninenines/ranch

下面通过改造ranch自带的reverse example实现简易的服务端。

game_server.app.src

{application, game_server, [
	{description, "Ranch TCP reverse example."},
	{vsn, "1"},
	{modules, []},
	{registered, []},
	{applications, [
		kernel,
		stdlib,
		ranch
	]},
	{mod, {game_server_app, []}},
	{env, []}
]}.

game_server_app.erl

-module(game_server_app).
-behaviour(application).
-export([start/2, stop/1]).

%% start/2
start(_Type, _StartArgs) ->
    {ok, _Pid} = ranch:start_listener(tcp_reverse, 1,
        ranch_tcp, [{port, 5555},{max_connections, 10240}], game_protocol, []),
    game_server_sup:start_link().

%% stop/1
stop(State) ->
    ok.

这里注意ranch:start_listener(Ref, NbAcceptors, Transport, TransOpts, Protocol, ProtoOpts) -> {ok, pid()} | {error, badarg}.

最大连接数max_connections就是在这里进行设定, 默认值1024. NbAcceptors, Acceptor的数量,具体数值要根据实际并发设置。

Ranch接受请求并建立连接,然后就会将具体的处理交给实现了ranch_protocol行为的game_protocol,erlang中的behaviour跟java中的接口差不多。

game_server_sup.erl

-module(game_server_sup).
-behaviour(supervisor).
-export([start_link/0, init/1]).

-spec start_link() -> {ok, pid()}.
start_link() ->
	supervisor:start_link({local, ?MODULE}, ?MODULE, []).

%% init/1
init([]) ->
	{ok, {{one_for_one, 10, 10}, []}}.

game_protocol.erl

-module(game_protocol).
-behaviour(gen_server).
-behaviour(ranch_protocol).

%% API.
-export([start_link/4]).

%% gen_server.
-export([init/4]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).

-define(TIMEOUT, 50000).

-record(state, {socket, transport}).

%% API.

start_link(Ref, Socket, Transport, Opts) ->
    proc_lib:start_link(?MODULE, init, [Ref, Socket, Transport, Opts]).

%% gen_server.

%% This function is never called. We only define it so that
%% we can use the -behaviour(gen_server) attribute.
init([]) -> {ok, undefined}.

init(Ref, Socket, Transport, _Opts = []) ->
    ok = proc_lib:init_ack({ok, self()}),
    ok = ranch:accept_ack(Ref),
    ok = Transport:setopts(Socket, [{active, once}, {packet, 2}]),
    gen_server:enter_loop(?MODULE, [],
        #state{socket=Socket, transport=Transport},
        ?TIMEOUT).

handle_info({tcp, Socket, Data}, State=#state{
        socket=Socket, transport=Transport}) ->
    io:format("Data:~p~n", [Data]),
    Transport:setopts(Socket, [{active, once}]),
    Transport:send(Socket, reverse_binary(Data)),
    {noreply, State, ?TIMEOUT};
handle_info({tcp_closed, _Socket}, State) ->
    {stop, normal, State};
handle_info({tcp_error, _, Reason}, State) ->
    {stop, Reason, State};
handle_info(timeout, State) ->
    {stop, normal, State};
handle_info(_Info, State) ->
    {stop, normal, State}.

handle_call(_Request, _From, State) ->
    {reply, ok, State}.

handle_cast(_Msg, State) ->
    {noreply, State}.

terminate(_Reason, _State) ->
    ok.

code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

%% Internal.

reverse_binary(B) when is_binary(B) ->
    list_to_binary(lists:reverse(binary_to_list(B))).

这里init的实现与常规的gen_server不一样。首先来说为什么不能用常规的gen_server写法。常规写法如下:

init([Ref, Socket, Transport, Opts]) ->
    ok = ranch:accept_ack(Ref),
    ok = Transport:setopts(Socket, [{active, once}, {packet, 2}]),
    {ok, #state{socket=Socket, transport=Transport}}.

gen_server的start_link只有在init/1执行完毕后才会返回,但我们来看ranch:accept_ack(Ref):

-spec accept_ack(ref()) -> ok.
accept_ack(Ref) ->
    receive {shoot, Ref, Transport, Socket, AckTimeout} ->
        Transport:accept_ack(Socket, AckTimeout)
    end.

运行ranch:accept_ack/1时,进程会阻塞,等待{shoot, ...}这条消息,直到接收到此消息才会继续执行,接着才会完成init。但是{shoot, ...}这条消息从哪里来?查下ranch源码不难发现,ranch在建立了与新的gen_server进程的连接后,会向gen_server进程发送该消息(参考ranch_conns_sup:loop/4). 显然,gen_server进程在等待ranch:accept_ack接收到{shoot,...}消息迟迟不能返回,而ranch又无法与gen_server进程连接发送不了{shoot,
...}消息,造成死锁。故使用proc_lib:start_link/3优雅地解决了此问题。

下面copy一下文档的一个说明:

By default the socket will be set to return `binary` data, with the

options `{active, false}`, `{packet, raw}`, `{reuseaddr, true}` set.

These values can‘t be overriden when starting the listener, but

they can be overriden using `Transport:setopts/2` in the protocol.

It will also set `{backlog, 1024}` and `{nodelay, true}`, which

can be overriden at listener startup.

这也就是为什么{active, once}, {packet, 2}只能在procotol里重写

这样就实现了一个基本的服务端,make后编写脚本启动:

start.sh

erl -pa ebin deps/*/ebin +K true +P 199999     -sname game_server     -s game 

-s game表示启动时默认调用game:start/0方法。

game.erl

-module(game).

%% ====================================================================
%% API functions
%% ====================================================================
-export([start/0, stop/0]).

start() ->
    ok = application:start(ranch),
    ok = application:start(game_server).

stop() ->
    application:stop(ranch),
    application:stop(game_server).

如果设置{packet, raw}的话,直接打开一个Terminal $ telnet localhost 5555 就可以进行测试了。

不过这里设置的{packet,2}, 所以写了个测试client发送消息,建立连接->发送消息->接收返回消息->关闭连接:

-module(client).

-export([send/1]).

send(BinMsg) ->
    SomeHostInNet = "localhost",
    {ok, Sock} = gen_tcp:connect(SomeHostInNet, 5555,
                                 [binary, {packet, 2}]),
    ok = gen_tcp:send(Sock, BinMsg),
    receive
        {tcp,Socket,String} ->
            io:format("Client received = ~p~n",[String]),
            gen_tcp:close(Socket)
        after 60000 ->
            exit
    end,
    ok = gen_tcp:close(Sock).

handler_info中加入不同消息的处理,就可以时间一个简单的游戏服务器了。R17后可以使用{active, N}, 程序效率应该会更高。

使用ranch tcp开发服务端

时间: 2024-08-14 22:32:24

使用ranch tcp开发服务端的相关文章

socket 网络编程快速入门(二)教你编写基于UDP/TCP的服务端多线程通信

在上一篇博文中,我们介绍了利用socket进行简单的UDP/TCP的服务端和客户端的通信. (一) 在基于UDP的程序中,你有没有想过,如果我的这台主机在通讯的时候要求既能够收到别的主机发来的数据,又能够自己向目的主机发出数据,该怎样实现?也就是说需要两个while循环同时进行.答案是使用多线程,一个线程用于接受数据,另一个线程用来发送数据.接下来我们介绍WinSock的多线程编程. 多线程的实现我们使用_beginthread()函数: uintptr_t _beginthread( void

linux 网络编程之最简单的tcp通信服务端

编写一个最为简单的tcp通信服务端.代码如下: #include <iostream> #include <cstring> using namespace std; #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <arpa/inet.h> #include <errno.h> #include <uni

TCP/IP网络编程之基于TCP的服务端/客户端(二)

回声客户端问题 上一章TCP/IP网络编程之基于TCP的服务端/客户端(一)中,我们解释了回声客户端所存在的问题,那么单单是客户端的问题,服务端没有任何问题?是的,服务端没有问题,现在先让我们回顾下服务端的I/O代码 echo_server.c --while ((str_len = read(clnt_sock, messag, 1024)) != 0) write(clnt_sock, messag, str_len);-- 接着,我们回顾客户端的代码 echo_client.c -- wr

利用TCP 客户端----&gt;服务端 传送文件到指定路径,并返回一个友好的回馈

首先盲写的一个传输文件的方法,但测试发现了一个非常不容易发现的问题,这里先说明一下. 错误的代码如下: 1 package com.TCP.java; 2 3 import java.io.File; 4 import java.io.FileInputStream; 5 import java.io.FileNotFoundException; 6 import java.io.FileOutputStream; 7 import java.io.IOException; 8 import j

动手学习TCP:服务端状态变迁

上一篇文章介绍了TCP状态机,并且通过实验了解了TCP客户端正常的状态变迁过程. 那么,本篇文章就一起看看TCP服务端的正常状态变迁过程 服务端状态变迁 根据上一篇文章中的TCP状态变迁图,可以得到服务器的正常状态变迁流程如下: CLOSED -> LISTEN -> SYN_RECV -> ESTABLISHED -> CLOSE_WAIT -> LAST_ACK -> CLOSED 具体的将状态跟TCP包关联起来就如下表示: From State To State

java TCP客户端 服务端 互访

服务端: 1, 创建socket服务器服务,服务器端为了让客户端可以连接上,必须提供端口,监听一个端口 2,获取客户端对象,通过客户端的socket流和对应的客户端进行通信 3,获取客户端的socket流的读取流 4,读取数据并显示在服务器端 5,关闭资源 package cn.net.tcp; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.

UDP广播 与 TCP客户端 --服务端

随着倒计时的响声,自觉无心工作,只想为祖国庆生. 最近有遇到过这样一个问题,将摄像头识别的行人,车辆实时显示在客户端中.有提供接口,会以Json的数据的形式将实时将识别的对象进行Post提交.所以我们这边先写一个web服务来持续接收数据,再将数据进行解析存入数据库.到这里为止,数据没有问题,都全部存入数据库中,这样还剩下一个实时刷新识别图片的问题.之前的处理方法是每隔5秒左右去读取数据库最新消息,用Timer计时器来解决,这样的话确实能解决问题,但是感觉不是最好的方法,因为摄像头识别的对象有时效

go使用TCP连接服务端错误

自己写了一个跑在Linux服务端的go程序,从客户端连接时提示:No connection could be made because the target machine actively refused it.然后关闭防火墙该,双向ping都能同,依然心灰意冷.但作为程序小白,百度了很多都无用,看了go的文档,在示例显示监听端口的代码net.Listen("tcp", ":8080"),我以前的监听代码net.Listen("tcp", &q

TCP协议:服务端和客户端demo--【J2SE】

服务端: import java.net.*; import java.io.*; public class TCPServer{ public static void main(String[] args)throws Exception{ ServerSocket ss=new ServerSocket(6666); while(true){ Socket s =ss.accept();//侦听并接受到此套接字的连接.阻塞式等待 DataInputStream dis=new DataInp