单机百万连接有多种方式, 这里采用一个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(){
@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() {
@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 {
@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"消,利用这种机制,我们就可以客户端和服务器之间的长连接
ulimit -n
这里一般显示的是65535,代表一个进程能够打开的最大文件数,一条TCP连接,对应Linux系统里面是一个文件,最大连接数会受限于这个数字,我们要做百万连接,所以需要修改这个值。 打开 /etc/security/limits.conf文件中配置如下两行:
hard nofile 1000000
soft nofile 1000000
soft和hard为两种限制方式,其中soft表示警告的限制,hard表示真正限制,nofile表示打开的最大文件数。
[root@database 100W]# cat /proc/sys/fs/file-max
1610630
vi /etc/sysctl.conf
cat /proc/sys/net/ipv4/ip_local_port_range
值为32768 61000
echo "1024 65535"> /proc/sys/net/ipv4/ip_local_port_range
现在有64511个端口可用.
net.ipv4.ip_local_port_range= 1024 65535
sysctl -p
依次在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台客户机,相信在服务器程序不做任何改变的情况下,也能完好支持,完全能够实现单台服务器百万长连接。
其实一般情况下,大规模连接并不是一个特别复杂的问题,以上只是采用了很简单的编程方法就能实现。 当然某些情况下的高并发的确是有难度的,例如春晚,瞬间可能有几百万的连接进来,而不是像上文那种,半个小时才有百万连接,上面这种编程肯定是不能应对的。