用Netty实现单机百万TCP长连接

一  服务实现模型

单机百万连接有多种方式, 这里采用一个netty server 占用8888 端口,用客户端机器模拟百万客户端连接 模拟实现的方式

以下是示意图

          

  如果一台客户端模拟3万个连接,那么100万连接,大致需要33台主机,找到33台主机的确是个困难,但是这种模型定下来,能够先实现若干台主机模拟连接也行,毕竟模型定了,剩下的只是客户主机数量的问题。

二 服务端编码

server.java

package com.jacklearn.server;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

public class Server 
{
    public static void main( String[] args )
    {    
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup(4);
        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(bossGroup, workerGroup);
        bootstrap.channel(NioServerSocketChannel.class);
        bootstrap.childOption(ChannelOption.SO_REUSEADDR, true);
        bootstrap.childHandler(new ChannelInitializer<SocketChannel>(){
            @Override
            public void initChannel(SocketChannel ch)throws Exception{
                ch.pipeline().addLast(new EchoServerHandler());
            }
        });

        bootstrap.bind(8888).addListener((ChannelFutureListener) future -> {
            System.out.println("bind success in port: " + 8888);
        });
        System.out.println("server started!");
    }

}
每当客户端连接上之后,则向客户发送“hello” 消息
package com.jacklearn.server;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;

@ChannelHandler.Sharable
public class EchoServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelActive(ChannelHandlerContext ctx){
        System.out.println("new client arrvie");
        ctx.writeAndFlush(Unpooled.copiedBuffer("hello", CharsetUtil.UTF_8));  //激活后立即发送hello 消息
    }


    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception{
        ByteBuf in = (ByteBuf) msg;
        ctx.writeAndFlush(in);
    }


    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause){
        ctx.close();
    }
}

三  客户端编码

package com.jacklearn.client;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.timeout.IdleStateHandler;


public class Client {

    private static final String SERVER_HOST = "a.b.c.d"; // 请注意用实际地址替换a.b.c.d

    public static void main(String[] args) {

        System.out.println("client starting....");
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup(4);
        final Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(eventLoopGroup);
        bootstrap.channel(NioSocketChannel.class);
        bootstrap.option(ChannelOption.SO_REUSEADDR, true);
        bootstrap.handler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel ch) {
                ch.pipeline().addLast(new IdleStateHandler(0, 0, 120)).
                        addLast(new EchoClientHandler());
            }
        });

        // 创建30000个客户端连接
        for (int i = 0;  i < 30000; i++) {
            try {
                ChannelFuture channelFuture = bootstrap.connect(SERVER_HOST, 8888);
                channelFuture.addListener((ChannelFutureListener) future -> {
                    if (!future.isSuccess()) {
                        System.out.println("connect failed, exit!");
                    }
                });
                channelFuture.get();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        try{
            System.in.read();
        }catch (Exception e){

        }
    }
}

 

package com.jacklearn.client;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.util.CharsetUtil;

import java.nio.charset.Charset;


public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {

    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
        ByteBuf buf = msg.readBytes(msg.readableBytes());
        System.out.println("Client received:" + ByteBufUtil.hexDump(buf) + "; The value is:" + buf.toString(Charset.forName("utf-8")));
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {

        if (evt instanceof IdleStateEvent) {
            IdleStateEvent e = (IdleStateEvent) evt;
            switch (e.state()) {
                case READER_IDLE:
                    break;
                case WRITER_IDLE:
                    break;
                case ALL_IDLE: //如果规定时内没有读写时间,则触发一个 消息
                    ctx.writeAndFlush(Unpooled.copiedBuffer("hello", CharsetUtil.UTF_8));  //激活后立即发送hello 消息
                    break;
                default:
                    break;
            }
        }
    }

}

这里之所以创建3万个连接,而不是创建5万连接,是因为在实测过程中,如果创建5万连接,CPU使用率会很高,影响其他程序。因此我们只创建3万个连接。

利用 IdleStateHandler ,如果120秒内客户端检测和服务器之间没有读写消息, userEventTriggered 会被触发, 此时我们向服务器发送 "hello"消,利用这种机制,我们就可以客户端和服务器之间的长连接

三  服务器和客户端配置

1. 修改server 的配置

1.1 突破局部文件句柄的限制

ulimit -n

这里一般显示的是65535,代表一个进程能够打开的最大文件数,一条TCP连接,对应Linux系统里面是一个文件,最大连接数会受限于这个数字,我们要做百万连接,所以需要修改这个值。 打开 /etc/security/limits.conf文件中配置如下两行:

hard nofile 1000000
soft nofile  1000000
soft和hard为两种限制方式,其中soft表示警告的限制,hard表示真正限制,nofile表示打开的最大文件数。

1.2 突破全局文件句柄的限制

[root@database 100W]# cat /proc/sys/fs/file-max
1610630
vi /etc/sysctl.conf

 
 

1.3 启动 server 

 

2. 启动客户端

2.1 修改客户机设置

cat /proc/sys/net/ipv4/ip_local_port_range  
值为32768 61000
  • 大概也就是共61000-32768=28232个端可用,单个IP对外只能发送28232个TCP请求。
  • 以管理员身份,把端口的范围区间增到最大
echo "1024 65535"> /proc/sys/net/ipv4/ip_local_port_range 

现在有64511个端口可用.

  • 以上做法只是临时,系统下次重启,会还原。 需要做永久修改, vi  /etc/sysctl.conf文件,增加一行内容
net.ipv4.ip_local_port_range= 1024 65535
sysctl -p

2.2  启动客户端程序

依次在N台服务器上部署客户端程序,并启动程序

四. 观察和验证

我启动了11台客户端机器,理论上每台机器应该有3万个连接,服务器有33万个连接

实际情形是,11台客户机器几乎同时启动,耗时半个小时左右, 服务器上总计有324699个连接, cpu 占用率不超过7%

[root@server 100W]# lsof -i:8888 | wc -l
324699

抽查了部分客户端机器,基本都是3万个长连接

[root@client1 ~]#  lsof -i:8888 | wc -l
30001

经过长时间的原型服务器基本稳定维持在32万长连接,客户机维持3万长连接 ,理论和实际相符

本次实验,服务端只实现了32万个长连接,但 这是由于客户机数量不足的原因,如果再能增加20台客户机,相信在服务器程序不做任何改变的情况下,也能完好支持,完全能够实现单台服务器百万长连接。

五 后记

其实一般情况下,大规模连接并不是一个特别复杂的问题,以上只是采用了很简单的编程方法就能实现。 当然某些情况下的高并发的确是有难度的,例如春晚,瞬间可能有几百万的连接进来,而不是像上文那种,半个小时才有百万连接,上面这种编程肯定是不能应对的。

 

  • 8
    点赞
  • 75
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
Netty中使用TCP长连接后,可以通过以下步骤来返回消息: 1. 创建一个消息类,用于封装需要返回的数据。 2. 在服务器端,编写处理请求的业务逻辑代码,并将返回的数据封装到消息类中。 3. 将消息类序列化为字节流,然后通过Netty的ChannelHandlerContext将字节流写回到客户端。 4. 在客户端中,编写处理返回消息的代码,并对接收到的字节流进行反序列化,得到消息类对象。 5. 根据消息类中的数据,对返回结果进行处理。 以下是一个简单的例子: 在服务器端: ```java public class ServerHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { // 处理请求,得到返回结果 String result = processRequest((String) msg); // 将返回结果封装到消息类中 Message message = new Message(result); // 将消息类序列化为字节流 byte[] bytes = Serializer.serialize(message); // 将字节流写回到客户端 ctx.writeAndFlush(Unpooled.copiedBuffer(bytes)); } } ``` 在客户端: ```java public class ClientHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { // 对接收到的字节流进行反序列化,得到消息类对象 Message message = (Message) Serializer.deserialize((byte[]) msg); // 根据消息类中的数据,对返回结果进行处理 processResponse(message.getResult()); } } ``` 其中,Message是一个自定义的消息类,Serializer是一个序列化工具类,processRequest和processResponse分别是处理请求和返回结果的业务逻辑代码。
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值