各位骚年好,我又来了.
上次写文章是啥时候? 忘了,也懒得翻记录,总之,很久没写知乎文章了
其实吧,不是很愿意把这个所谓的碎碎念记下的一些笔记(很多还是从其他地方抄的,做了一些修改和补充)叫做所谓的“文章”.感觉是对这两个字的侮辱. 或许叫做学习笔记,可能更合适,不过先这么称呼吧,总比“震惊,26岁大叔居然对Netty做出这样的事情...”这类丧心病狂的标题好一些.
回想自己年轻的时候,在那本科的青葱岁月里,我还是一个非常英俊,头发茂密的少年.追求者排成了一条长龙,从杭州市,一直排到了银河系外(此处是幻想).
然而,作为一个要在计算机领域有所建树的有志青年(此处是痴心妄想!),“两耳不闻窗外事,一心只敲心中码”.在每一个炎热的中午,专注地和大学室友构思着一件惊天动地,百思不解的大事:“午饭去哪个食堂吃饭,吃些什么!”然后回来继续做着我们的“网络编程”大作业:“利用windows C socket ,自定通讯协议.完成一个即时通讯工具,按照能支撑的并发连接数量,评定最终成绩”.
好家伙,我终于可以和“腾讯”一决高下了!(不知天高地厚)
中间过程略过不提,最终是采用了windows的重叠异步IO端口模型(windows平台的IOCP),在Acer电脑(4G内存,i3双核处理器上支持2万个并发连接),整个组全部拿到了优秀评级.
这样就说明自己真的优秀了吗? 其实自己做的事情真没多少,只是定义了一些通讯协议,根据编程模型,调用了windows提供的底层API,并做了一些比较繁琐的调用和事件处理操作.平时工作如果叫做搬砖的话,那么这个最多就是“花式搬砖”.
不过,就这个“调用windows C socket API”这个操作,说起来简单,调用过程还是相当繁琐的.大量的样板代码.(当初的代码不知道哪里去了,不然可以贴上来一段).在C/C++领域,不知道是否有成型的通讯框架(很久没接触这块了),但是好在Java对这些繁琐的操作做了封装,实现了Netty框架.对外屏蔽了很多繁琐的操作,让一般开发者不用关心底层的接口调用.
我相信很多骚年应该都会听说过Netty(就算没有真正使用过).特别是去网上搜索“高并发网络编程 Java”,然后你就会搜索到Netty,随着Dive into 的过程,哈哈哈,你就会像我一样,去某个地方记一下笔记
进入正题:
先介绍一下Socket先生,它是无人不知,无人不晓,统领TCP和UDP两位小弟,在网络界里驰骋风云.典型的高富帅人物.在计算机的世界里,两个的非同一个主机进程之间(IP地址+端口标记一个进程,我们可以称做为“端点Point”)想要进行友好沟通,必须劳烦Socket进行消息传达工作.对于我们要传达的消息,我们需要告诉socket,对方的端口号,以及IP地址(也就是传说中的“点对点通信”)
在服务端的样板代码会是这么个样子
ServerSocket serverSocket = new ServerSocket(1111);//这里的1111是服务端的socket监听端口号
Socket clientSocket = serverSocket.accept();//等待客户端向服务端发起请求
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(),true);
String request,response;
while (((request= in.readLine())!=null)){//不断循环等待客户端发送的消息
if("Done".equals(request)){
break;
}
response = request;
out.println(response);//将客户端发来的消息原封不动地发送回去
}
启动上面的代码,我们可以用命令工具 telnet做一个小测试:
其中红色的helloWorld是客户端(也就是这个命令行界面)发送给上述的服务端的消息
其中绿色的helloWorld是服务端返回给该客户端的消息
这个时候我们再起一个客户端,进行同样的操作,这个时候新的客户端不管发什么消息,都将得不到响应,问题在于上述代码中等待监听的 serverSocket.accept()在收到一个客户端的连接后,将进入到后续代码的while (((request= in.readLine())!=null))中,循环等待这个客户端发送的信息,不在理会新的连接请求(也无法理会,应为监听代码accpet已经执行过去了)
这个时候,我们来改造一下,改成下面的代码:
ServerSocket serverSocket = new ServerSocket(1111);
while (true) {
Socket clientSocket = serverSocket.accept();
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
String request, response;
while (((request = in.readLine()) != null)) {
if ("Done".equals(request)) {
break;
}
response = request;
out.println(response);
}
}
这么做还是存在问题,当一个客户端连接进来,并且不终端连接的情况下,代码将始终处在第二个wihle循环中,不断等待这个客户端发送数据,而没有机会在此执行到accept方法,除非这个客户端终止以后,下一个客户端才能被处理.
综上,上述的情况,一次只能接受一个客户端请求,那么,我有办法同时接受多个客户端请求吗?
有! 为每一个连接进来的客户端创建一个单独的线程来响应处理. 大概的代码是这样子的
ServerSocket serverSocket = new ServerSocket(1111);
while(true) {
Socket clientSocket = serverSocket.accept();
//为每一个客户端连接创建一个线程处理,主线程只负责接受客户端的连接请求
Runnable connectionHandler = new Runnable() {
@Override
public void run() {
try {
myProcess(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
};
new Thread(connectionHandler).start();
}
好了,上述代码可以同时处理接受多个客户端的连接了. 世界大和谐!!!
哈哈,开什么玩笑,如果就这么简单,我特么写这个流水账干什么.
在Java的世界里,新创建一个线程默认会分配64KB以上的内存(大概值),假设一个服务器有16GB内存,抛开其他一切因素,最多可以创建16*1024*1024/64= 262144个线程
等等,你真的打算就通过开线程来粗暴地完成多个客户端的连接工作吗? 我觉得你计算机的老师会把你打断狗腿.先不说完全开不出这么多线程,大量的线程在cpu上执行调度,需要进行上下文切换,但线程数量很大时,CPU的主要任务将集中在线程调度的工作上,而不再有精力去执行线程应该完成的任务.(强烈建议没有学过操作系统的骚年们,去系统学一下操作系统)
“让我们考虑一下这种方案的影响。第一,在任何时候都可能有大量的线程处于休眠状态,只是等待输入或者输出数据就绪,这可能算是一种资源浪费。第二,需要为每个线程的调用栈都分配内存,其默认值大小区间为64 KB到1 MB,具体取决于操作系统。第三,即使Java虚拟机(JVM)在物理上可以支持非常大数量的线程,但是远在到达该极限之前,上下文切换的开销就会带来麻烦,例如,在达到10 000个连接的时候”(摘录来自: [美] Norman Maurer Marvin Allen Wolfthal. “Netty实战。” iBooks. )
如果思考上面的代码,实际上是因为阻塞的IO操作(阻塞等待客户端连接进来,和发送消息),所以需要用每一个线程来包装每一个阻塞操作,使得“阻塞等待”在线程之间编程了非阻塞的异步操作,然而每一个线程内部,还是阻塞等待的状态.那么如果将阻塞的I/O改成为非阻塞的I/O也就能避免创建过多线程的情况. 这个时候,我们有请NIO上台表演一下.
来,直接把宝贝掏出来给你们看
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();//创建一个NIO的Selector
ServerSocketChannel serverSocket = ServerSocketChannel.open();//创建serverChannel,就是创建了一个NIO版的Socket
serverSocket.bind(new InetSocketAddress("localhost", 1111));//将该socket绑定到本地1111端口进行监听
serverSocket.configureBlocking(false);//置通道为非阻塞模式
serverSocket.register(selector, SelectionKey.OP_ACCEPT);//注册对 Accept操作的订阅
ByteBuffer buffer = ByteBuffer.allocate(256);
while (true) {
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();//得到已经I/O就绪的key
Iterator<SelectionKey> iter = selectionKeys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
if (key.isAcceptable()) {//如果是客户端新的连接
register(selector, serverSocket);
}
if (key.isReadable()) {//如果是已连接客户端的新数据
answerWithEcho(buffer, key);
}
iter.remove();
}
}
}
public static void register(Selector selector, ServerSocketChannel serverSocket) throws IOException {
SocketChannel client = serverSocket.accept();//为客户端新的连接请求创建socket
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);//为这个客户端socket订阅"数据就绪"(有新的数据发过来,就是一个"数据就绪")
}
private static void answerWithEcho(ByteBuffer buffer, SelectionKey key) throws IOException {
SocketChannel client = (SocketChannel) key.channel();
client.read(buffer);
if ("DONE".equals(new String(buffer.array()).trim())) {
client.close();
System.out.println("Closed");
}
buffer.flip();
client.write(buffer);//将客户端的数据原样返回客户端
buffer.clear();
}
上述代码用一幅图来描述的话,就是下面这个样子
上述模型是使用Java的NIO的改造,对于大量的并发可以使用更少的线程,从而达到更好的性能.对于NIO不了解的同学,可以移步我的另一篇学习笔记 玩转Java NIO
预计下次更新时间为本周末