分类

课内:
不限
类型:
不限 毕业设计 课程设计 小学期 大作业
汇编语言 C语言 C++ JAVA C# JSP PYTHON PHP
数据结构与算法 操作系统 编译原理 数据库 计算机网络 软件工程 VC++程序设计
游戏 PC程序 APP 网站 其他
评分:
不限 10 9 8 7 6 5 4 3 2 1
年份:
不限 2018 2019

资源列表

  • 基于汇编实现的贪吃蛇游戏

    一 需求分析现在有的一些人感觉生活都是很无聊的,所以有些时候肯定会玩各种各样的游戏的,有一些大的游戏,玩起来会话掉很多的时间,而且也会花掉大量精力的 ,所以在一些闲暇的时候一些小游戏会博得很多人的喜爱,例如:俄罗斯方块,和一些格斗游戏等等。然而这些小游戏的设计方法和软件有很多,所以也有很多不同的效果,本篇设计是采用汇编中宏定义和调用,子程序的调用等一些汇编知识编制而成的一个贪吃蛇小游戏,通过这个小游戏的制作,我们可以得到很多的益处,一方面我们可以不在浪费平时的一些空闲的时间了,而热中于我们自己喜欢的游戏;另一个方面我们制作完游戏还可以给自己或者别人来享受一下,同时自己也会有一种成就感。特别当你用自己学到的知识制作出一个东西的时候,或者克服一个困难的时候你就会发现你自己的价值所在了,而且这还能促进你喜欢学习的念头。
    二 程序设计2.1 设计思想这个程序的总体的思想也就是主要用的就是宏和子程序的定义和调用:

    首先,定义了5个宏它们分别为:

    屏幕初始化宏定义在光标位置显示字符和属性定义显示字符串宏定义置光标位置宏定义读光标位置的字符和属性宏定义,它们在程序中起到主要的作用
    其次,就是子程序定义了,程序中定义了很多的子程序,其中有:

    控制子程序食物子程序,还有记分子程序等它们首先被主程序调用,然后它们之间再互相调用,这样构成了完整的游戏程序并实现其功能

    具体的设计思想

    第一,对数据进行初始化,即对寄存器的初始化,比如对食物的初始化等
    第二,开始运行,判断是否符合条件,如果符合赢的条件的话,就会跳转到赢的那个子程序下,然后那个赢的子程序会调用记分的那个子程序,最后显示赢的信息和分数后再转到控制程序执行控制和其后的程序。如果要是输的话,就会跳转到输的那个子程序下,然后那个输的子程序也会调用记分的那个子程序,最后显示得分和信息。如果要是没有赢也没有输的话,那程序会跳到控制的子程序中,等控制完以后程序又会跳到程序判断的那个地方重复的执行

    2.2 程序流程
    三 程序实现3.1 实现环境
    系统:Windows
    开发工具:MASM5.0版本

    3.2 实现思想这个程序完成的功能就是和别的游戏一样,最初,给出一个初始化的界面,和一个红的小心,我们要把那个最初给出那个小链条穿过那个小红心,然后那个红心就会变成我们那个小链条上的组成单元一样的一个小黄圆,和在别的地方会在出现另一个小红心,我们接下来要做的就是要把那个小黄圆穿过来,然后我们那个小链条就会变长了,然后在穿过小红心,就这样重复的做就行了,随着你的穿过你的小链条就会越来越长了,这是你的得分就会越高,同时你难度也会变大的,因为如果你要碰到四周的变的话,或者你自己的那个小链条首尾要是相连的话你就会输了,所以当你那个小链条很长的时候,难度自然就会变大了。
    3.3 汇编过程
    把源程序汇编成.OBJ文件。方法是:在DOS环境中找到.ASM的目标文件,然后输入MASM *.ASM,然后按回车就可以生成目标文件.OBJ了
    再输入LINK .OBJ,在按回车就可以得到可执行文件.EXE文件了

    四 运行测试在我们用的软件MASM的目录下,我们可以找到.EXE文件,双击就可以打开了。再有就是在DOS的环境下,在.EXE的文件所在的位置,直接输入.EXE的文件名就可以了,比如说,如果我们的.EXE文件在C:\MASM\下,我们就可以在DOS环境下在C:\MASM\后直接输入.EXE的文件名,就可以了。
    我们通过键盘的上下左右键来控制里面我们的那个小链条,即键盘上的方向键,只需要这四个键就可以了。
    游戏画面一

    游戏画面二

    五 参考文献[1] IBM-PC 汇编语言程序设计 沈美明等 清华大学出版社
    [2] 8086/8088宏汇编语言程序设计教程 第二版 王正智/编电子工业出版社
    [3] 80X86汇编语言程序设计教程 杨季文/等编 清华大学出版社
    1 评论 23 下载 2018-11-03 15:40:55 下载需要1点积分
  • 基于Netty和WebSocket的Web聊天室

    一、背景伴随着Internet的发展与宽带技术的普及,人们可以通过Internet交换动态数据,展示新产品,与人进行沟通并进行电子商务贸易。作为构成网站的重要组成部分,留言管理系统为人们的交流提供了一个崭新的平台。同时,聊天室作为一个新型的Web应用程序,为互联网用户提供了一个实时信息交流的场所。
    聊天室在早期的网络时代已经非常流行,例如BBS、IRC这些类似的机制。它为互联网用户提供了实时对话的功能,并因此成为了非常流行的网络服务。网络会议和网上聊天均可以通过聊天室来实现。聊天室为互联网用户提供了一个更好的交友环境,这种交友形式类似于互联网化的笔友,但是大大节省了信件传送时间。对于网站留言管理而言,目前非常受欢迎的做法是基于JAVA WEB和脚本语言,并结合动态网页和数据库,然后通过应用程序来处理信息。
    网络聊天系统利用了现代的网络资源和技术,为人们的交流和联系提供了一个平台,用以加快信息化建设,促进人和人之间的交流和沟通。Internet存在于全球范围,它将世界各地大小的网络连接成了一个整体,万维网目前已经成为了世界上最大的信息资源宝库,它是一种更容易被人们接受的信息检索方式。根据估算,目前在Internet上存在数以万计的网站,内容包括文化、金融、教育科研、新闻出版、商业、娱乐等。它的用户群是非常庞大的,所以建立一个好的网站非常重要。
    以前旧的联系方法已经不能满足现代人的生活。网上聊天系统因其方便的沟通方式而成为了重要且实用的计算机应用程序。系统管理者通过提供完整的网上聊天系统管理,来促进人们之间相互沟通与交流。
    实时显示聊天者的谈话内容是聊天室最重要的特点之一。所谓的实时性与常的留言板和讨论区有很大的不同,它是指同一个聊天室内的用户可以在很短的时间内立即看到其他用户的留言。随着计算机技术的快速发展,现在可以使用Java Web+HTML方便快速地开发出一个典型的聊天室程序。但是还需要花费更多的心思,获得更强大的聊天功能来吸引更多的网络用户。
    二、目的与要求本程序实现一个基于Web的多人聊天室程序,访客可以自由加入聊天室,并设定自己的昵称。
    开发要点:采用浏览器端和服务器端(B/S)的开发技术。利用浏览器解析HTML语言达到即时聊天作用,无需考虑操作系统环境等外部因素。服务器开发使用JAVA面向对象的开发方法进行开发与设计,通过采用高性能的Netty框架+WebSocket协议搭建即时聊天服务器,可以支持起高并发稳定交互。
    三、开发环境
    软件:

    操作系统:Windows 10
    Java开发IDE:Intellij IDEA 2016.2.4

    HTML/JS/CSS开发IDE:Sublime Text 3测试浏览器:Google Chrome 版本 58.0.3029.110 (64-bit)测试服务器:Tomcat 8.0.22
    硬件:

    处理器:Intel® Core(TM) i5-5200U CPU @ 2.20GHz 2.19GHz控制器:Intel® 9 Series Chipset Family SATA AHCI Controller
    内存(RAM):8.00 G

    四、框架介绍4.1 Netty 5.X 框架Netty是一个高性能、异步事件驱动的NIO框架,它提供了对TCP、UDP和文件传输的支持,作为一个异步NIO框架,Netty的所有IO操作都是异步非阻塞的,通过Future-Listener机制,用户可以方便的主动获取或者通过通知机制获得IO操作结果。作为当前最流行的NIO框架,Netty在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用,一些业界著名的开源组件也基于Netty的NIO框架构建。
    Netty,为了尽可能提升性能,Netty采用了串行无锁化设计,在I/O线程内部进行串行操作,避免多线程竞争导致的性能下降。表面上看,串行化设计似乎CPU利用率不高,并发程度不够。但是,通过调整NIO线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列-多个工作线程模型性能更优。——摘取自《Netty高性能之道》

    4.2 WebSocket 协议Websocket是html5提出的一个协议规范,是为解决客户端与服务端实时通信而产生的技术。websocket协议本质上是一个基于tcp的协议,是先通过HTTP/HTTPS协议发起一条特殊的http请求进行握手后创建一个用于交换数据的TCP连接,此后服务端与客户端通过此TCP连接进行实时通信。
    WebSocket API最伟大之处在于服务器和客户端可以在给定的时间范围内的任意时刻,相互推送信息。 浏览器和服务器只需要要做一个握手的动作,在建立连接之后,服务器可以主动传送数据给客户端,客户端也可随时向服务器发送数据。

    4.3 Bootstrap 框架Bootstrap,来自 Twitter,是目前最受欢迎的前端框架。Bootstrap 是基于 HTML、CSS、JAVASCRIPT 的,它简洁灵活,使得 Web 开发更加快捷。
    Bootstrap框架的优点:

    移动设备优先:自 Bootstrap 3 起,框架包含了贯穿于整个库的移动设备优先的样式
    浏览器支持:所有的主流浏览器都支持 Bootstrap
    容易上手:只要您具备 HTML 和 CSS 的基础知识,您就可以开始学习 Bootstrap
    响应式设计:Bootstrap 的响应式 CSS 能够自适应于台式机、平板电脑和手机
    其他优点:为开发人员创建接口提供了一个简洁统一的解决方案;包含了功能强大的内置组件,易于定制;还提供了基于 Web 的定制,并且是开源

    五、功能流程图
    六、功能分析6.1 支持昵称登录用户通过浏览器访问服务器时,需要确定自己的昵称,便于交流。

    其次调用JS脚本代码来检查用户输入是否为空,代码如下:
    function userLogin() { if (!userNick) { userNick = $('#nick').val().trim(); } if (userNick) { //其他逻辑事务处理 ………………………… } else { $('#tipMsg').text("连接没有成功,请重新登录"); $('#tipModal').modal('show'); }
    若游客昵称验证成功,则进行是否支持WebSocket判断:
    if (window.WebSocket) { window.socket = new WebSocket("ws://localhost:9090/websocket"); window.socket.onmessage = function (event) { }; window.socket.onclose = function (event) { console.log("connection close!!!"); closeInvake(event); }; window.socket.onopen = function (event) { console.log("connection success!!"); openInvake(event); };} else { alert("您的浏览器不支持WebSocket!!!");}
    6.2 支持多人同时在线聊天室支持多人登录而不轻易崩溃。由于Netty框架封装的高性能NIO特性,可以明显看到多用户同时在线时交流时的流畅性。相比较于SpringMVC、Jetty等框架,Netty在多线程交互这方面上有很大的优化。
    利用测试软件Selenium模拟100位用户同时登录聊天,由于篇幅原因只展示前22位用户的列表。

    6.3 同步显示在线人数和成员列表聊天室支持多人登录,实时更新在线人数和列表,页面展示如下:

    用户退出时即时发布广播消息:

    6.4 支持文字和表情的内容聊天室支持在线用户发送文字与表情内容:

    6.5 浏览器与服务器保持长连接,定时心跳检测为了保证浏览器与服务器保持长连接,定时进行心跳检测,详情可见“项目技术分析的“心跳检测机制部分”。

    七、项目技术分析7.1 握手和安全认证系统在浏览器和服务器TCP链路建立成功通道激活时,握手信息会由浏览器自动发起构造空消息体的协议内容(详情看“私有协议开发”)。握手请求发送后,按照协议规范,服务端需要返回握手应答消息。
    switch (data.extend.code) { case 20001: // user count //业务处理 break; case 20002: // auth console.log("auth result: " + data.extend.mess); isAuth = data.extend.mess; if (isAuth) { //认证成功 $("#menuModal").modal('hide'); $('#chatWin').show(); $('#content').append('欢迎来到聊天室!!<hr/>'); }
    7.2 心跳检测机制
    在握手成功后,有服务器端轮询机制发送心跳消息,浏览器收到心跳消息之后,返回应答消息。由于心跳消息的目的是为了检测链路的可用性,因此不需要携带消息体
    服务器端轮询机制:

    //定时任务:扫描所有的Channel,关闭失效的Channel executorService.scheduleAtFixedRate(new Runnable() { @Override public void run() { logger.info("scanNotActiveChannel --------"); UserInfoManager.scanNotActiveChannel(); } }, 3, 60, TimeUnit.SECONDS); //定时任务:向所有客户端发送Ping消息 executorService.scheduleAtFixedRate(new Runnable() { @Override public void run() { UserInfoManager.broadCastPing(); } }, 3, 50, TimeUnit.SECONDS);

    服务器端调用扫描方法scanNotActiveChannel()确保每个TCP链路的激活:
    public static void scanNotActiveChannel() { Set<Channel> keySet = userInfos.keySet(); for (Channel channel : keySet) { UserInfo userInfo = userInfos.get(channel); if (userInfo == null) continue; if (!channel.isOpen() || !channel.isActive() || (!userInfo.isAuth() && (System.currentTimeMillis() - userInfo.getTime()) > 10000)) { removeChannel(channel); } } }

    服务器端调用广播方法broadCastPing()向每个连接发生Ping消息:
    try { rwLock.readLock().lock(); //加锁 Set<Channel> keySet = userInfos.keySet(); for (Channel channel : keySet) { UserInfo userInfo = userInfos.get(channel); if (userInfo == null || !userInfo.isAuth()) continue; channel.writeAndFlush(new TextWebSocketFrame(ChatProto.buildPingProto())); } } finally { rwLock.readLock().unlock(); //解锁 }

    浏览器在WebSocket连接处理器收到Ping消息后,调用chat.js文件中的pingInvake()返回Pong消息给服务器:
    //浏览器处理ping消息function pingInvake(data) { //发送pong消息响应 send(isAuth, "{'code':10012}");};

    服务器收到来自浏览器的Pong消息后,调用updateUserTime()方法来更新标记和用户登录时间:
    public static void updateUserTime(Channel channel) { UserInfo userInfo = getUserInfo(channel); if (userInfo != null) { userInfo.setTime(System.currentTimeMillis()); }}
    浏览器端收到Ping消息请求,并发起Pong消息应答:

    服务器收到Pong消息后更改标志:

    7.3 WebSocket协议整合Netty基于HTTP协议栈开发了WebSocket协议栈,利用Netty的WebSocket协议栈可以很方便地开发出WebSocket客户端和服务器端。

    WebSocket服务器端开发(以UserAuthHandler类为例):
    public class UserAuthHandler extends SimpleChannelInboundHandler<Object> { private static final Logger logger = LoggerFactory.getLogger(UserAuthHandler.class); private WebSocketServerHandshaker handshaker; @Override protected void messageReceived(ChannelHandlerContext ctx, Object o) throws Exception { //传统HTTP接入 if (o instanceof FullHttpRequest) { handleHttpRequest(ctx, (FullHttpRequest) o); } //WebSocket接入 else if (o instanceof WebSocketFrame) { handleWebSocket(ctx, (WebSocketFrame) o); } } /** * 重写心跳检测机制 * @param ctx * @param evt * @throws Exception */ @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { //判断evt事件是不是IdleStateEvent事件 if (evt instanceof IdleStateEvent) { IdleStateEvent evnet = (IdleStateEvent) evt; //判断是读空闲事件还是写空闲事件还是读写空闲事件 if (evnet.state().equals(IdleState.READER_IDLE)) { //读操作发起操作 final String remoteAddress = NettyUtil.parseChannelRemoteAddr(ctx.channel()); logger.warn("NETTY SERVER PIPELINE: IDLE exception [{}]", remoteAddress); //移除用户并更新数量 UserInfo userInfo = UserInfoManager.getUserInfo(ctx.channel()); UserInfoManager.removeChannel(ctx.channel()); UserInfoManager.broadCastInfo(Constants.SYSTEM_USER_COUNT,UserInfoManager.getAuthUserCount()); UserInfoManager.broadCastInfo(Constants.SYSTEM_USER_LIST, UserInfoManager.getUserInfoList()); if (null != userInfo){ UserInfoManager.broadCastInfo(Constants.SYSTEM_OTHER_INFO, "心跳检测发生异常,用户 "+userInfo.getNick()+"(" +userInfo.getUserId()+") 已经强制下线!<hr/>"); } } } ctx.fireUserEventTriggered(evt); } private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest request) { //对URL进行判断,如果HTTP解码失败,返回HTTP异常 if(!request.decoderResult().isSuccess() || !"websocket".equals(request.headers().get("Upgrade"))){ logger.warn("protobuf don't support websocket"); ctx.channel().close(); return; } WebSocketServerHandshakerFactory handshakerFactory = new WebSocketServerHandshakerFactory( Constants.WEBSOCKET_URL,null, false); handshaker = handshakerFactory.newHandshaker(request); if (handshaker == null){WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel()); } else { // 动态加入websocket的编解码处理 handshaker.handshake(ctx.channel(), request); UserInfo userInfo = new UserInfo(); userInfo.setAddr(NettyUtil.parseChannelRemoteAddr(ctx.channel())); // 存储已经连接的Channel UserInfoManager.addChannel(ctx.channel()); }} private void handleWebSocket(ChannelHandlerContext ctx, WebSocketFrame frame) { // 判断是否关闭链路命令 if (frame instanceof CloseWebSocketFrame) { handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain()); UserInfo userInfo = UserInfoManager.getUserInfo(ctx.channel()); System.out.println(userInfo.toString()); UserInfoManager.removeChannel(ctx.channel()); if (null != userInfo) { UserInfoManager.broadCastInfo(Constants.SYSTEM_OTHER_INFO, "用户 "+userInfo.getNick()+"(" +userInfo.getUserId()+") 退出聊天室!<hr/>"); } return; } // 判断是否Ping消息 if (frame instanceof PingWebSocketFrame) { logger.info("ping message:{}", frame.content().retain()); ctx.writeAndFlush(new PongWebSocketFrame(frame.content().retain())); return; } // 判断是否Pong消息 if (frame instanceof PongWebSocketFrame) { logger.info("pong message:{}", frame.content().retain()); ctx.writeAndFlush(new PongWebSocketFrame(frame.content().retain())); return; } //非文本(二进制)信息 if (!(frame instanceof TextWebSocketFrame)) { throw new UnsupportedOperationException(frame.getClass().getName() + " frame type not supported"); } String message = ((TextWebSocketFrame) frame).text(); JSONObject json = JSONObject.parseObject(message); int code = json.getInteger("code"); Channel channel = ctx.channel(); switch (code){ case Constants.PING_CODE: case Constants.PONG_CODE:{ UserInfoManager.updateUserTime(channel); logger.info("receive pong message, address: {}",NettyUtil.parseChannelRemoteAddr(channel)); return; } case Constants.AUTH_CODe:{ boolean isSuccess = UserInfoManager.saveUser(channel, json.getString("nick")); UserInfoManager.sendInfo(channel,Constants.SYSTEM_AUTH_STATE,isSuccess); if (isSuccess) { UserInfo userInfo = UserInfoManager.getUserInfo(channel); //更新在线人数UserInfoManager.broadCastInfo(Constants.SYSTEM_USER_COUNT,UserInfoManager.getAuthUserCount());UserInfoManager.broadCastInfo(Constants.SYSTEM_USER_LIST, UserInfoManager.getUserInfoList()); if (null != userInfo) { //增加人数 UserInfoManager.broadCastInfo(Constants.SYSTEM_OTHER_INFO, "用户 "+ userInfo.getNick() + "(" + userInfo.getUserId() + ") 进入网络聊天室,大家热烈欢迎~<hr/>"); } } return; } case Constants.MESS_CODE: //普通的消息留给MessageHandler处理 break; default: logger.warn("The code [{}] can't be auth!!!", code); return; } //后续消息交给MessageHandler处理 ctx.fireChannelRead(frame.retain()); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception{ logger.warn("NETTY SERVER PIPELINE: Unknown exception [{}]", cause.getMessage()); UserInfoManager.removeChannel(ctx.channel()); UserInfoManager.broadCastInfo(Constants.SYSTEM_USER_COUNT, UserInfoManager.getAuthUserCount()); UserInfoManager.broadCastInfo(Constants.SYSTEM_USER_LIST, UserInfoManager.getUserInfoList()); UserInfoManager.broadCastInfo(Constants.SYSTEM_OTHER_INFO, "网络发生未知错误,与部分用户断开连接,请确保网络正常!<hr/>"); }}
    服务器端代码解析:WebSocket第一次握手请求消息由HTTP协议承载,所以他是一个HTTP消息,执行handleHttpRequest方法来处理WebSocket握手请求。当对握手请求消息进行判断,如果消息头中没有包含Upgrade字段或者它的值不是websocket(协议定义),则返回HTTP400响应。
    握手请求简单校验通过之后,开始构造握手工厂,创建握手处理类WebSocketServerHandshaker,通过它构造握手响应消息返回给客户端,同时将WebSocket相关的编码和解码类动态添加到ChannelPipeline中,用于WebSocket消息的编码与解码。
    添加了WebSocketEncoder和WebSocketDecoder之后,服务器就可以自动对于WebSocket消息进行编码解码,后面的业务handler可以直接对WebSocket对象进行操作。这样子,浏览器端与服务器端的TCP连接通道就可以建立,一旦浏览器发起对于服务器WebSocket接口的请求,就可以自动激活通道进行通信。

    WebSocket客户端开发:
    if (window.WebSocket) { window.socket = new WebSocket("ws://localhost:9090/websocket"); window.socket.onmessage = function (event) { var data = eval("(" + event.data + ")"); console.log("onmessage data: " + JSON.stringify(data)); switch (data.head) { // 私有协议栈开发内容 } }; window.socket.onclose = function (event) { // WebSocket断开触发的方法 console.log("connection close!!!"); closeInvake(event); }; window.socket.onopen = function (event) { // WebSocket开启时触发的方法 console.log("connection success!!"); openInvake(event); };
    关于WebSocket API的调用较为简单。创建一个Socket实例,参数为URL,ws表示WebSocket协议。onopen、onclose和onmessage方法把事件连接到Socket实例上。每个方法都提供了一个事件,以表示Socket的状态。
    onmessage事件提供了一个data属性,它可以包含消息的Body部分。消息的Body部分必须是一个字符串,可以进行序列化/反序列化操作,以便传递更多的数据。
    WebSocket对于大多数客户机-服务器的异步通信是理想的,在浏览器内聊天是最突出的应用。WebSocket由于其高效率,被广泛应用。
    7.4 线程安全使用和并发控制为了实现高并发下的线程安全,该项目中应用了许多线程安全的数据结构与高并发控制.
    7.4.1 线程安全的自增/自减类 AtomicIntegerAtomicInteger,一个提供原子操作的Integer的类。在Java语言中,++i和i++操作并不是线程安全的,在使用的时候,不可避免的会用到synchronized关键字。而AtomicInteger则通过一种线程安全的加减操作接口。
    AtomicInteger提供原子操作来进行Integer的使用,因此十分适合高并发情况下的使用。例如,在构建用户实例时:
    //线程安全自动递增,产生UIDprivate static AtomicInteger uidGener = new AtomicInteger(1000);
    通过调用 uidGener 来构造用户唯一标识符(uid),确保能够识别每个接入服务器的用户。
    7.4.2多线程并发调用调优在Java多线程编程中,会在JVM进程结束前调用系统方法线程,通过重写线程方法类来确保进程释放所有资源再结束。
    Runtime.getRuntime().addShutdownHook():在jvm中增加一个关闭的钩子,当jvm关闭的时候,会执行系统中已经设置的所有通过方法addShutdownHook添加的钩子,当系统执行完这些钩子后,jvm才会关闭。所以这些钩子可以在jvm关闭的时候进行内存清理、对象销毁等操作。
    // 注册进程钩子,在JVM进程关闭前释放资源 Runtime.getRuntime().addShutdownHook(new Thread(){ @Override public void run(){ server.shutdown(); logger.warn(">>>>>>>>>> jvm shutdown"); System.exit(0);
    7.4.3 优雅退出线程在多线程编程中,很经常开启/结束线程,对于结束线程时,除了调用JVM强制性的清理方法外,经常需要重写线程退出方法,以降低对于系统资源的消耗。以ChatServer类为例:
    public void shutdown() { if (executorService != null) { executorService.shutdown(); } super.shutdown(); }
    7.4.4 线程池调度与轮询调度Java通过Executors提供四种线程池,这里主要调用了newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
    线程池的主要作用就是限制系统中执行线程的数量。根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果;少了浪费了系统资源,多了造成系统拥挤效率不高。用线程池控制线程数量,其他线程排 队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程池 中有等待的工作线程,就可以开始运行了;否则进入等待队列。通过调用线程池减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。同时可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
    在初始化服务器实例时,懒加载线程池:
    public ChatServer(int port){ this.port = port; //创建一个定长线程池,支持定时及周期性任务执行 executorService = Executors.newScheduledThreadPool(2); }
    通过线程池的定时及周期性任务调度,执行项目系统的轮询机制:
    //定时任务:扫描所有的Channel,关闭失效的Channel executorService.scheduleAtFixedRate(new Runnable() { @Override public void run() { logger.info("scanNotActiveChannel --------"); UserInfoManager.scanNotActiveChannel(); } }, 3, 60, TimeUnit.SECONDS); //定时任务:向所有客户端发送Ping消息 executorService.scheduleAtFixedRate(new Runnable() { @Override public void run() { UserInfoManager.broadCastPing(); } }, 3, 50, TimeUnit.SECONDS);
    轮询机制主要有两个,一个是每隔60s轮询所有Channel连接,确保网络链路连通;同时,每隔50s调度一次心跳检测,确保线程连接的活跃。
    7.4.5 读写锁的调用为了提高性能,Java提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。
    Java中读写锁有个接口java.util.concurrent.locks.ReadWriteLock来获得读写锁。但使用Java读写锁也是有条件的:重入方面其内部的WriteLock可以获取ReadLock,但是反过来ReadLock想要获得WriteLock则永远都不要想;WriteLock可以降级为ReadLock,顺序是:先获得WriteLock再获得ReadLock,然后释放WriteLock,这时候线程将保持Readlock的持有.反过来ReadLock想要升级为WriteLock则不可能;ReadLock可以被多个线程持有并且在作用时排斥任何的WriteLock,而WriteLock则是完全的互斥.这一特性最为重要,因为对于高读取频率而相对较低写入的数据结构,使用此类锁同步机制则可以提高并发量;不管是ReadLock还是WriteLock都支持Interrupt;WriteLock支持Condition并且与ReentrantLock语义一致,而ReadLock则不能使用Condition,否则抛出UnsupportedOperationException异常。
    例如,在UserInfoManager处理类中启用读写锁:
    //设置读写锁private static ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(true); public static void removeChannel(Channel channel){ try {//加写锁 rwLock.writeLock().lock(); //业务处理 } finally { rwLock.writeLock().unlock(); //解锁 } }
    7.4.6 安全的数据结构项目系统中调用了许多安全的数据结构,比如ConcurrentHashMap类。
    concurrentHashmap是为了高并发而实现,内部采用分离锁的设计,有效地避开了热点访问。而对于每个分段,ConcurrentHashmap采用final和内存可见修饰符volatile关键字(内存立即可见:Java 的内存模型可以保证:某个写线程对 value 域的写入马上可以被后续的某个读线程“看”到。注:并不能保证对volatile变量状态有依赖的其他操作的原子性)。
    例如,在UserInfoManager处理类中启用安全的数据结构ConcurrentHashMap类。
    //构建安全的数据结构 private static ConcurrentMap<Channel, UserInfo> userInfos = new ConcurrentHashMap<Channel, UserInfo>();
    八、私有协议开发8.1 私有协议定义私有的所有的消息都一个Json字符串,格式如下:{head | body | extend}

    head:作为头部,用int类型储存,长度为4个字节
    body:消息的有效载体,用String类型储存,长度无限制
    extend:协议的扩展字段,用Map类型储存,value值为Object对象

    8.2 私有协议属性8.2.1 协议头定义


    名称
    数值
    描述
    备注




    PING_PROTO
    1 << 8 \
    220
    ping消息(476)



    PONG_PROTO
    2 << 8 \
    220
    pong消息(732)



    SYST_PROTO
    3 << 8 \
    220
    系统消息(988)



    EROR_PROTO
    4 << 8 \
    220
    错误消息(1244)



    AUTH_PROTO
    5 << 8 \
    220
    认证消息(1500)



    MESS_PROTO
    6 << 8 \
    220
    普通消息(1756)



    PRIV_PROTO
    7 << 8 \
    220
    私聊消息(2012)
    暂未使用



    8.2.2 消息类型定义


    名称
    数值
    描述
    备注




    AUTH_CODE
    10001
    认证消息类型



    MESS_CODE
    10002
    普通消息类型



    PING_CODE
    10011
    Ping消息类型



    PONG_CODE
    10012
    Pong消息类型



    SYSTEM_USER_COUNT
    20001
    在线用户数



    SYSTEM_AUTH_STATE
    20002
    认证结果



    SYSTEM_OTHER_INFO
    20003
    系统通知类型



    SYSTEM_USER_LIST
    20004
    在线用户列表



    8.2.3 HTTP响应消息


    数值
    描述
    备注




    1xx
    指示消息
    请求已接受收,继续处理


    2xx
    成功消息
    请求已被接收、理解、接受


    3xx
    重定向消息
    要完成请求需要更进一步的操作


    4xx
    客户端错误消息
    请求有语法错误或者请求无法实现


    5xx
    服务器端错误消息
    服务器未能处理请求



    九、总结此次项目,我是使用了Netty框架,而且是现在最新的5.0.0.1版本,这是一次考验我对Netty框架掌握程度的一次挑战。因为已经学习Java语言接近一年半了,可一直以来都是只学习关于后端领域的知识,此次项目过程中也暴露了我对于前端领域掌握的薄弱。虽然今天的互联网公司都提倡前后分离,但前后端毕竟是要一起合作的,了解前端知识有助于后端人员更好与前端合作。从这个方面看来,这又是对于我本身的一次挑战。
    通过此次项目,更加加深了我对Java语言的认识,这对我后面的学习和工作都将打下基础。在项目中遇到不知名的困难、遇到无法实现的前端效果,自己一个人默默坚持,利用好庞大的网络资源,从中寻找解决方法。虽然有许多想要实现的功能与想法,苦于自己知识的薄弱点无法实现,这是一个遗憾,也是一次鞭策。这,要求我继续努力、坚持、奋斗,在不断地学习与实践中不断完善自己。
    1 评论 19 下载 2018-11-03 14:13:15 下载需要8点积分
  • 基于JAVA的远程屏幕监控系统

    摘 要远程屏幕监控系统在生活中是很常见的,学校机房的机房管理系统、PC版QQ的远程演示功能等都属于远程屏幕监控系统。监控系统的原理是通过客户端不断的截取屏幕发送到服务器端,服务器端进而将画面呈现出来的过程。本论文实现的是一个多客户端的远程屏幕监控系统。
    本论文第一部分对系统进行项目分析,包括需求分析、可行性分析、相关技术分析,大致介绍了整个项目需要做的工作以及需要掌握的技术,介绍了Socket通信原理、截屏原理、Swing树、系统托盘、自定义JPanel实现显示监控图像以及多线程的知识。
    第二部分分别对系统托盘模块、自定义协议模块、获取屏幕截图模块、连续发送与接收图片模块、登录、退出模块、多客户端处理模块、Swing树模块、自定义JPanel模块进行介绍。我没有直接搬上一大堆的理论知识,而是先简要介绍模块功能,然后按照正常思考的思路去实现项目需要的功能,并且去分析实现这个功能的必要性。遇到问题之后就分析出现这个问题的原因以及考虑如何去提升效率、减少存储空间等一系列优化问题。然后通过最后的分析给出一个优化后的解决方案,同时我将自己当时思考的错误点也罗列了出来,对多个处理方法都给予了尝试。针对每个模块都给出了功能的实现详细步骤以及示例代码。
    第三部分是Web服务器环境配置以及程序使用说明。本项目是远程屏幕监控系统,如果要测试的话,服务器端的程序是需要部署在服务器上的,所以我将本机Web服务器环境配置的方法也讲解一下,另外还有关于本程序代码如何打包等知识都有讲解。
    第四部分是我在写项目的过程中的犯的一些错误以及项目的难点,第五部分是对该系统后续的一些功能的设想,第六部分是我的一些感想,第七部分是项目运行效果的展示。
    关键字:屏幕监控;Socket;Swing;自定义协议;Web服务器环境配置
    一、项目分析1.1 需求性分析项目的初始阶段就是对整个系统进行预估,这有利于我们对整个系统的理解,屏幕监控系统需要实现的功能有:

    客户端登录、退出
    客户端截屏以及连续发送图像
    客户端系统托盘功能
    服务器端连续接收图像以及客户端其他请求
    服务器端显示连接用户的用户树
    客户端退出后用户树刷新
    客户端发送图像后显示在服务器端

    1.2 可行性分析需求性分析里提到的功能能否实现呢?我们在这里进行讨论:

    通过构造自定义协议实现,都是通过将这些请求构造成协议从而发送到服务器
    截屏功能通过Robot类实现,然后将BufferedImage转化为字节数组输出流,再转化为字节数组,并以协议的方式发送到服务器实现图像的连续发送
    使用系统托盘对象SystemTray来实现
    可以通过自定义协议工具类提供的解析数据的方法解析出数据,并根据消息类型进行相应的处理
    用户树使用JTree实现,DefaultTreeCellRenderer可以设置树的外观,为JTree设置节点选中监听器可以监听节点选中事件
    用DefaultTreeModel的reload()方法实现
    可以自定义JPanel,通过paint(g)方法绘制图片

    1.3 技术点分析1.3.1 Socket网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket,java的API提供了对Socket的支持。
    1.3.2 自定义网络协议网络协议为计算机网络中进行数据交换而建立的规则、标准或约定的集合。为了满足我们的需求,我们需要自定义一个协议,并为其提供发送消息、解析消息的功能。
    1.3.3 系统托盘系统托盘是个特殊区域,通常在桌面的底部,项目中涉及到了对系统托盘的一些操作,我们为客户端提供系统托盘功能,可以方便用户关闭监控。
    1.3.4 IO流流是一种抽象概念,它代表了数据的无结构化传递。按照流的方式进行输入输出,数据被当成无结构的字节序或字符序列。从流中取得数据的操作称为提取操作,而向流中添加数据的操作称为插入操作,用来进行输入输出操作的流就称为IO流。换句话说,IO流就是以流的方式进行输入输出。我们主要使用的有DataOutputStream、DataInputSream、ByteArrayoOutputStream等。
    1.3.5 屏幕截图使用Robot类实现屏幕截取以及事件回放操作。
    1.3.6 AWT与SWING抽象窗口工具包,该包提供了一套与本地图形界面进行交互的接口,是Java提供的用来建立和设置Java的图形用户界面的基本工具;以抽象窗口工具包为基础使跨平台应用程序可以使用任何可插拔的外观风格。该项目主要是用到了窗口以及树控件、树的刷新、树的节点外观、节点选择事件处理等技术。
    1.3.7 自定义JPanelJPanel代表一个面板,通过实现一个继承自JPanel的DrawPanel,重写其paint(g)方法实现将图像画到视图上,如果不断修改绘制的图片,在速度达到的情况下可以实现屏幕监控画面显示的功能。
    1.3.8 多线程多线程是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。多线程是为了同步完成多项任务,不是为了提高运行效率,而是为了提高资源使用效率来提高系统的效率。
    二、功能实现2.1 系统托盘模块2.1.1 系统托盘是个什么东西?系统托盘是个特殊区域,通常在桌面的底部,在那里,用户可以随时访问正在运行中的那些程序。在微软的Windows里,系统托盘常指任务栏的状态区域;在每个系统里,托盘是所有正运行在桌面环境里的应用程序共享的区域。
    2.1.2 有必要实现系统托盘吗?回答是肯定的,当前的大部分软件都会提供一个系统托盘让用户更加方便的操作,QQ的系统托盘左键可以打开QQ窗口,右键可以选择退出账号、注销账号、更改状态等一系列操作客户端是负责将屏幕截图发到服务器以及执行一些收到的指令,也需要与服务器端做一些交互,比如:登录、发消息、退出等操作,如果把这些处理操作放到系统托盘里可以增大用户粘性,使用户可以更方便使用系统。
    2.1.3 怎么实现系统托盘?JAVA的API提供了一系列关于系统托盘的类与方法,为软件添加系统托盘功能的步骤如下:

    我们先把图片放到src同级目录下
    首先获取图片的Image
    根据Image创建托盘图标TrayIcon
    创建系统托盘对象SystemTray
    创建弹出菜单PopupMenu,并为其添加MenuItem以及为MenuItem添加点击事件
    为托盘图标TrayIcon添加弹出菜单PopupMenu
    为系统托盘SystemTray添加托盘图标

    2.1.4 实现系统托盘代码public void showSystemTray() { Image image = Toolkit.getDefaultToolkit().getImage("img/icon.png"); final TrayIcon trayIcon = new TrayIcon(image);// 创建托盘图标 trayIcon.setToolTip("屏幕监控系统\n客户端");// 设置提示文字 final SystemTray systemTray = SystemTray.getSystemTray();//托盘 final PopupMenu popupMenu = new PopupMenu(); // 创建弹出菜单 MenuItem item = new MenuItem("退出"); item.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { //菜单项点击处理逻辑 } }); popupMenu.add(item); trayIcon.setPopupMenu(popupMenu); // 为托盘图标加弹出菜单 try { systemTray.add(trayIcon); // 为系统托盘加托盘图标 } catch (AWTException e) { e.printStackTrace(); }}
    2.2 自定义协议2.2.1 网络协议网络协议为计算机网络中进行数据交换而建立的规则、标准或约定的集合。协议就是一个交换数据的规则,我们也可以自定义一个交换数据的规则,并按照规则进行数据的传输,这样我们就自定义了一个协议。
    2.2.2 为什么自定义协议?在客户端与服务器端进行Socket通信时,两者进行通信主要通过连接取得socket对象,在使用socket取得输入、输出流实现读取、写入数据功能。以客户端向服务器端发送一条消息为例:

    两者通过Socket建立连接,客户端有一个Socket对象,客户端连接成功后,服务器端也会得到一个Socket对象
    客户端得到Socket的输出流,往输出流中写字符串消息,客户端Socket关闭
    服务器端得到Socket的输入流,从输入流中读取出字符串消息,服务器端Socket关闭

    这样就实现了从客户端将消息发送到服务器端,但是如果发送多条消息就会出问题了,你会说在客户端写个死循环让它一直运行,并且发送消息之后不关闭Socket连接不就可以了吗?
    我最开始的想法也是这样的,在服务器端获取到连接的Socket后,循环接受客户端发送的消息,循环结束的条件是输入流中没有数据。
    这个方法对于文本类消息是可行的,但是客户端发送的是图片字节数组,如果还是这个方法,那么就要考虑读字节数组时读到什么位置算是读到了一张图片,想来想去,自定义协议是可以解决这个问题的。
    2.2.3 自定义协议

    type:消息类型
    totalLen:该数据的长度=消息长度+5字节
    bytes[]:实际的消息

    2.2.4 自定义协议相关类说明我定义两个工具类:Protocol、ResultProtocol类的静态常量如下表:



    常量
    含义




    TYPE_IMAGE
    发送的是图片


    TYPE_LOAD
    客户端登录


    TYPE_LOGOUT
    客户端退出



    Protocol类封装了协议的两个使用方法,方法签名如下:static void send(int,byte[],DataOutputStream)该方法将字节数组写到指定输出流中,第一个参数是消息类型(静态常量),第二个参数是字节数组型的数据,第三个参数是输出流。static Result getResult(DataInputStream)该方法从指定输入流中读取数据,返回一个包含了这些数据的Result对象
    Result类封装了一个消息的数据,包括type、totalLen、data[]
    2.2.5 核心代码//客户端登录功能Protocol.send(Protocol.TYPE_LOAD, "client".getBytes(), dos);//服务器端接收消息功能Result result =Protocol.getResult(dis);//以协议的规范向输出流中写数据dos.writeByte(type);dos.writeInt(totalLen);dos.write(bytes);dos.flush();//以协议的规范从输入流读取数据
    Protocol类的getResult方法核心:
    byte type = dis.readByte();int totalLen=dis.readInt();byte[] bytes=new byte[totalLen-4-1];dis.readFully(bytes);
    2.3 客户端模块2.3.1 获取屏幕截图BufferedImage bfImage = robot.createScreenCapture(new Rectangle(0, 0, width, height));
    其中width,height是屏幕的宽高,截取屏幕用的是JAVA提供的Robot类,Robot类可以模拟用户的行为,如控制鼠标、打字等一系列操作。所以可以使用Robot实现屏幕截图以及事件回放的功能。
    2.3.2 将图片以协议的规范发到服务器我们获取到了屏幕截图的BufferedImage,那么应该怎么将它传到服务器呢?先看我的初期想法:
    通过ImageIO将BufferedImage写到一个File里,然后从这个文件的输入流中读出数据至字节数组中,然后使用协议工具类把这条消息发出去就可以了。这里会产生问题:应该把BufferedImage写到哪个File,我们有三个选择:

    a.每次获得一个BufferedImage就新建一个File
    b.程序启动后初始化创建一个File
    c.写到临时文件中,并设置程序退出删除临时文件

    针对情况a,可以顺利的发送图片。但是由于获得屏幕截图后需要创建文件,向文件里写数据,然后再读出来,给我一种感觉就是好像做了重复的工作,效率不高;并且每得到BufferedImage后就创建一个File,因为系统每隔50ms就会截取一张图片,每张图片都大于1M。我测试的时候仅仅运行了十多分钟我的一个磁盘就被写满了,这样势必会产生两个问题:效率问题、空间占用问题。
    针对情况b,系统在获取BufferedImage后往一个固定的File里写数据,从这个File里读出数据,这个方法只生成一个文件,然后不断的在这个文件里进行读写操作,这样处理的话内存占用确实会少很多,但效率不会提升,并且在实际中使用的时候发现系统会报异常:文件损坏错误。异常的原因其实也很简单,因为屏幕截取的速率是很快的,截图之后就会往File里写数据,如果此时上张图片还没有读完,那么就会导致文件损坏。
    针对情况c,经我试验,任何用处都没有,临时文件在程序结束后也没有删除,虽然这个方法失败了,但也是一种尝试。
    经查阅资料发现,通过直接将BufferedImage转化为字节数组从而达到我们对空间、速率上的要求,以下是具体过程:
    经查阅资料发现ImageIO类可以直接将BufferedImage对象写到字节数组输出流中,然后我们在将字节数组输出流的数据转化为字节数组就可以了。
    ByteArrayOutputStream baos = new ByteArrayOutputStream();ImageIO.write(buffedImage, "png", baos);Protocol.send(Protocol.TYPE_IMAGE, baos.toByteArray(), dos);
    2.3.3 系统退出机制我为客户端的托盘提供了[右键菜单>退出]选项,客户端有一个布尔类型的变量标志是否生存,系统会检测这个值然后执行循环:截图,发送,休眠。当用户选中退出时,系统先想服务器发送一个退出请求,然后更改变量标志为false,并关闭掉socket连接以及输入、输出流,正常退出。为了让系统更加的健壮,我们要对系统异常捕获以及处理异常。
    2.3.4 客户端代码final Client client = new Client(); client.conn("192.168.1.101", 33000);//连接服务器 client.load();//登录 client.showSystemTray();//显示托盘 while (client.isLive) { client.sendImage(client.getScreenShot()); try { Thread.sleep(50); } catch (InterruptedException e) { System.exit(0); } }
    2.4 服务器端模块2.4.1 服务器逻辑服务器端程序运行后,创建ServerSocket,然后不断的接受连接上的Socket,每当客户端连接上,就将Socket交到一个线程手里,由该线程负责该客户端的所有交互行为,服务器应该有一个Map保存客户端IP与Socket的对应关系
    服务器端接受到客户端发来的登录请求,将它的IP作为Key,Socket作为Value保存到Map中,将连接上的用户显示在控制界面View的用户树上
    服务器端接收到客户端发送的发送图片请求,将字节数组转化为BufferedImage,将这张图片重绘到控制界面的屏幕监控视图上
    ByteArrayInputStream bai=new ByteArrayInputStream(data);BufferedImage buff=ImageIO.read(bai);//为屏幕监控视图设置BufferedImageServer.view.centerPanel.setBufferedImage(buff); Server.view.centerPanel.repaint();
    服务器接受到客户端发送的退出请求,从控制界面View的用户树上删除该客户端IP,从Map中删除该客户端IP,关闭当前客户端的Socket,释放掉资源。
    2.4.2 对多客户端的处理界面分为两部分:左侧的用户树,右侧的监控区域。
    用户树显示当前连接到的所有客户端IP地址,右侧的监控区域显示当前监控的客户端的屏幕图象。
    在一个客户端的情况下,服务器端接收到客户端的图像就显示在监控区域上。当有多个客户端连接的时候,如果服务器端不对消息进行过滤的话,那么在监控区域上会轮流显示各个客户端的屏幕监控,所以需要在服务器端标记当前监控的IP地址,当有客户端发过来图像的话,将客户端IP与标记的IP进行比对,如果相同才把图像显示出来,否则就将消息丢弃掉。这里要涉及到几个知识点:用户树的刷新、用户树的选中事件处理、用户树的节点样式
    我们用以下的方式创建一棵树
    model=new DefaultTreeModel(root);JTree tree=new JTree(model);//刷新用户树DefaultMutableTreeNode node1=new DefaultMutableTreeNode(nodeString);root.add(node1);model.reload();//用户树节点选中事件的处理tree.addTreeSelectionListener(new TreeSelectionListener() { @Override public void valueChanged(TreeSelectionEvent e) { JTree tree=(JTree) e.getSource(); DefaultMutableTreeNode selectionNode = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent(); String nodeName=selectionNode.toString(); Server.curKey=nodeName; }});
    用户树的节点样式是通过DefaultTreeCellRenderer来实现的
    DefaultTreeCellRenderer cr=new DefaultTreeCellRenderer();cr.setBackgroundNonSelectionColor(Color.darkGray);cr.setTextNonSelectionColor(Color.white);tree.setCellRenderer(cr);
    2.4.3 服务器端对客户端消息请求的处理客户端与服务器的一系列交互在客户端连接上之后就被交予一个线程来全权负责了,服务器端通过协议工具类的解析数据的方法获取到消息类型、消息长度以及消息内容,将消息类型以及消息内容交予一个函数来处理
    Result result =Protocol.getResult(dis);handleType(result.getType(),result.getData());private void handleType(int type,byte[] data) { switch (type) { case 1://处理图片请求 break; case 2://处理登录请求 break; case 3://处理退出请求 break; }}
    三、Web服务器环境配置3.1 简述本系统分为两个端:server端、client端,使用时,应该将Server端部署在服务器上,服务器可以使用远程服务器,也可以使用本机PC作为服务器进行测试,以下以本机PC搭建局域网服务器为例
    3.2 工具win7系统的笔记本
    3.3 搭建过程
    开始>控制面板>程序与功能>打开或关闭window功能>展开Internet信息服务,选择Web管理工具、万维网服务以及其子选项,点击确定
    开始>控制面板>管理工具>Internet信息服务管理器>点击默认的网站,配置网站路径、端口之后即可访问了

    3.4 使用方法
    查询服务器IP

    在服务器端打开命令提示符,输入ipconfig,找到其IP地址,即IPV4地址
    修改代码

    本代码客户端默认连接ip为127.0.0.1的服务器,即本机,所以我们修改这个IP。在Client类中有一个main方法,它是程序的入口,当客户端连接时是调用Client对象的conn方法,只需要将这个方法中的参数修改为查询到的服务器IP地址即可
    程序打包

    项目下有三个包:Server、Client、Util,需要注意的是Util包下存放的是客户端和服务器都需要用到的工具类,所以不管是服务器还是客户端在打包时都需要包含Util包,另外客户端用到了系统托盘图标,所以打包时需要用到图片,需要包含img文件夹
    程序运行

    在搭建好web环境的PC机上运行服务器端,在其他电脑上运行客户端要注意先运行服务器端程序,再运行客户端程序,客户端与服务器端建立连接成功后会显示系统托盘,服务器端运行后会有一个窗口,当有用户连接上时在服务端右侧界面会显示客户端PC机屏幕,当有多个客户端连接到服务端时,左侧用户树会显示所有客户端IP,点击树节点就会切换监控用户

    四、项目难点4.1 客户端循环发送图片的问题图片是字节数组,循环发送的话会导致服务器端找不到图片截止的标志,无法获取图片。就是因为这个需求我才会想到自定义协议,通过构造一个协议发到服务器,服务器就可以知道读取到什么位置是一张图片。为了性能与空间上的要求,我们需要找一种可以直接把图片转化为字节数组的方法。
    ByteArrayOutputStream baos = new ByteArrayOutputStream();ImageIO.write(buffedImage, "png", baos);
    转化之后baos.toByteArray()就可以将其转化为字节数组。
    以下方法可以将字节数组转化为图片:
    ByteArrayInputStream bai=new ByteArrayInputStream(data);BufferedImage buff=ImageIO.read(bai);
    4.2 服务器端线程里操作JPanel重绘在线程里操作,那么要把这个Panel设为静态的,程序里有好几个地方都需要设置成静态的,如保存客户IP与Socket的Map、View的实例对象。
    这是自定义的一个继承自JPanel的类DrawPanel,根据我们的需求,我们要在这个JPanel上不断绘制从客户端传回来的图像,所以该类需要提供一个方法来设置绘制的图像。方法签名如下:
    void setBufferedImage(BufferedImage bufferedImage)
    将bufferedImage设置为当前要绘制的BufferedImage:
    void paint(Graphics g)
    重写该方法,内部是利用g绘制当前的BufferedImage到视图上。
    当操作JPanel重绘时只需要两步就可以了:

    调用该类实例对象的setBufferedImage(BufferedImage bufferedImage)方法设置要绘制的图像
    调用实例对象的repaint()方法重绘JPanel

    4.3 对异常的处理客户端异常退出时会造成空指针异常、CG异常、连接关闭异常、IO异常、文件读写异常等,这些异常都是在项目中出现的。在出现异常后,不要惊慌,首先根据系统提示一步一步排查,当排查到某行代码上时,仔细分析出现错误的原因,比如空指针异常,那么要考虑变量值为什么是空,要考虑变量是否是静态的,要发散思维看待问题。为了使系统更加健壮,我们要捕获异常以及对异常进行处理。
    五、展望任何一个优秀的作品都需要不断的改进,那么我来说一下因为时间原因来不及完成的几个功能以及大概实现思路,也算是对这个系统的展望吧。
    5.1 事件回放功能事件回放功能是当客户端与服务器端建立连接后,客户端向服务器发送屏幕截图的同时,将客户端的鼠标、键盘等事件一起发送出去。当服务器接收到之后,利用Robot对象对整个事件进行事件回放处理,那么服务器端的监控区域不仅仅只显示客户端的屏幕,而且还有鼠标、键盘等事件。
    5.2 远程控制功能目前只实现了远程监控功能,在服务器端只能看到客户端发生了什么,但并不能对客户端进行任何操作。由于在上边定义了一个协议,所以实现这个功能也很简单。只需要在拓展几种消息类型即可,服务器端确定要控制的客户端IP,并将服务器端的事件封装之后用协议工具类将消息发送到客户端,客户端根据消息类型进行判断,如果是控制类型的指令,那么就将事件对象在客户端进行回放。这样就实现了一个简单的控制功能。
    5.3 聊天模块其实论文写到这里聊天模块的实现已经没有难度了,其实在客户端登录的时候就用到了这个功能。客户端登录是向服务器发送了一个字符串,只不过服务器端接收到消息后并没有回应。如果客户端让用户输入字符串,系统将消息发送到服务器端,服务器端对消息进行提醒、显示,并给服务器端提供一个输入区域,将回应的消息发送到指定IP的客户端。那么一个简单的1对1的即时聊天模块就算实现了。
    5.4 UI优化现在的系统UI太粗糙了,因为Swing本身做UI不是很有优势,我对这方面了解的不是很深。对UI优化可以使用已有的LookAndFeel或者使用其他的开源库。后续可以尝试为服务器端界面加上工具栏以及图标、点击效果。
    5.5 Log日志记录Log日志记录功能是当系统运行时将Log日志输出到log文件中,Log日志保存的是一个软件的运行状态。
    六、感想通过做这个项目发现对零碎的小知识点掌握的不牢固,例如图片与字节数组的转换、系统托盘实现等一些功能都是现学现用的。其中在线程中操作View中的对象是其中最让我头疼的地方,因为那个对象没有用static导致一直获取不到数据,一下子就困惑了一天。目前系统已经完成了,回头想想,整个系统的开发并不是很难,流程很清晰,整个的难点就是那个循环发送字节数组的问题,使用自定义协议这个方法后问题也就解决了。项目开发中可能会遇到一些奇怪的事情,但是不要着急,一步一步缩小查找范围还是可以找到错误的原因的。项目主要用到了Socket通信原理,socket通信有很多的应用,我们可以用socket做即时通信软件、联网游戏、文件上传下载工具等等。突然发现学的东西还是挺有用的,在这里要谢谢任课老师在课堂上对知识点进行认真的讲解,我从中学到了很多。
    总之呢,写软件是个很磨人的过程,我们可能会一直犯错,有时候感觉自己看出错在哪了,改正过之后竟然还是有错,并且有时候的错误会使我们感觉很无奈。如果能够坚持下来写几个小项目,那么对自己的能力绝对是一个大的提升。
    附 录图1:服务器端运行,此时无客户端连接

    图2:服务器端运行,此时有客户端连接,左侧为用户树,树节点为客户端IP,右侧为监控区域,显示的是被控端的屏幕画面
    1 评论 55 下载 2018-11-03 13:54:55 下载需要8点积分
  • 基于java的人机五子棋

    1 任务设计书本项目要实现的是五子棋人机版,通过制定棋型的评分表使机器能够对棋盘局势评估。五子棋玩家双方分别称为“人”、“机器” ,当人落子后,机器对棋盘扫描获取可行棋的位置集合,然后遍历该集合,利用评估函数对每个空位依次估分,得分最高的位置即为机器要落子的位置,在使用评估函数对空位打分时,为了避免机器只攻不守,需要使用“换位思考”的思想,也就是说打分时不仅考虑自身,还要考虑对方。
    2 类与对象的设计2.1 类2.1.1 位置实体类LocationLocation类封装棋盘上的一个位置,AI对局势分析时会对位置打分,所以位置实体类应该有个字段保存位置分数,Location类的设计如图1所示。


    public Location(int x, int y)
    构造函数。x:横坐标,y:纵坐标
    public Location(int x, int y, int player)
    构造函数。x:横坐标,y:纵坐标,player:位置所有者
    public Location(int x, int y, int player, int score)
    构造函数。x:横坐标,y:纵坐标,player:位置所有者,score:位置分数
    public void setX(int x)
    设置横坐标的值
    public void setY(int y)
    设置纵坐标的值
    public void setScore(int score)
    设置位置分数
    public void setPlayer(int player)
    设置该位置由玩家player落子,player可取:Chess.PLAYER、Chess.AI
    public int getX()
    获取对象的横坐标
    public int getY()
    获取对象的纵坐标
    public int getPlayer()
    获取该位置是由哪位玩家所有
    public int getScore()
    获取该位置的分数

    2.1.2 自定义棋盘类ChessPanelChessPanel类负责视图上的事情,如棋盘以及棋子的绘制、棋盘状态的保存、落子、清空等事件,ChessPanel类的设计如图2所示。


    public void paint(Graphics g1)
    重写该方法,绘制棋盘、棋子
    public void drawBoard(Graphics2D g)
    绘制棋盘
    public void drawChessman(Graphics2D g)
    绘制棋子
    public void clearBoard()
    清空棋盘
    public void doPlay(int row, int col, int player)
    玩家在视图上落子

    2.1.3 控制器类ChessChess类主要负责逻辑上的各个事件,如逻辑上落子、AI局势分析、胜负判定等,Chess类的设计如图3所示。


    public void setFirst(int first)
    设置先手的玩家
    public Chess()
    构造函数,进行棋局的初始化
    public void restart()
    棋局初始化
    public Location start()
    AI先手时调用,决策第一手棋位置
    public boolean play(int x, int y, int player)
    玩家落子。x,y是落子坐标,player的取值:Chess.AI、Chess.PLAYER,返回值表示是否落子成功
    public void showToConsole()
    显示棋盘信息到控制台
    private void addToList(List\<Location> allMayLocation, int x, int y)
    添加位置到可行棋的位置集合,过滤重复的位置
    public Location explore()
    返回分数最高的位置
    private List\<Location> getAllMayLocation()
    得到可行的位置集合
    public boolean isWin(int x, int y, int cur)
    判断胜负,x、y是落子位置,cur是Chess.AI、Chess.PLAYER
    public int getScore(int x, int y)
    局势评估函数,计算总得分
    private int getScoreBySituation(int count1, int leftStatus, int rightStatus)
    根据棋型计算空位得分,count1为相连棋子数,leftStatus、rightStatus为1或2,1代表为空,2为墙或者对方棋子
    public int getXScore(int x, int y, int cur)
    横向扫描计算得分
    public int getYScore(int x, int y, int cur)
    纵向扫描计算得分
    public int getSkewScore1(int x, int y, int cur)
    正斜向扫描计算得分
    public int getSkewScore2(int x, int y, int cur)
    反斜向扫描计算得分

    2.1.4 视图类ViewView类主要负责游戏窗口的显示以及对Chess、ChessPanel进行调度,通过调用控制器的方法来控制逻辑上的棋局,通过调用ChessPanel的方法来控制视图上的棋局,View类的设计如图4所示。


    public void create()
    创建视图
    public void restartBoard()
    棋局重开
    public void showChess(ChessPanel chessPanel, MouseEvent e)
    鼠标点击落子事件处理,chessPanel是已实例化的棋盘面板对象,e是鼠标事件

    2.1.5 测试类TestTest类是程序的入口,提供了主函数,其主要负责实例化视图类View对象,Test类的设计如图5所示。

    2.2 对象以四个对象为例,画出对象图,每个图中第一格是对象名:类名,第二格是对象拥有的属性,对象的设计如图6所示。

    2.3 对象之间的关系view对象是视图类的实例,chessPanel是棋盘类的实例,pos是位置实体类的实例,chess是控制器类的实例。可以说view是游戏的控制中枢,负责调度chessPanel和chess;chessPanel负责游戏视图;chess是负责游戏逻辑;pos表示一个位置,可以使用Location的集合来保存棋盘的状态。
    3 主要方法的实现3.1 实现原理我们时常会想,五子棋AI是怎么确定下在哪个位置上的,以下分析一下实现的原理。玩家落子后会形成一个局势,在该局势下AI依次为每个空位打分,得分最高的位置就是AI落子的位置。

    问题一:空位那么多,遍历加上计算的开销可不小,那么怎么提高效率?问题二:怎么为空位打分?
    问题一解决:按照原理,每当玩家落子后AI需要遍历所有空位为其打分,其实有很多空位是可以直接排除掉的,每次都要计算一些无用的空位,所以在时间上就造成了浪费。玩过五子棋的人都知道,除第一手棋外,我们的棋子一般下到其他棋子的附近,也就是说那些远离棋子的空位是不需要考虑的,只有那些在非空位附近的位置才是需要考虑的,那么怎么定义“附近”这个概念呢?
    设有某非空位置pos1,以pos1为中心呈米字型所能覆盖到的每一个空位pos2都可以称为是pos1附近的位置,整个棋盘所有非空位附近的位置构成一个无重复的集合称为可行位置集合。

    从图中可以看到,AI只需要搜索与非空位附近的位置,减少了很多不必要的搜索、计算,虽然随着棋局的深入这种方法依然需要搜索非常多的位置,但是在这个过程中已经排除掉了不少的位置,运算效率已经有了提升。
    问题二解决:为空位打分我们需要定义一张评分表作为评分的标准,假设为某空位pos打分,首先AI在这个位置试探性的放一枚己方棋子,AI为白方。
    3.1.1 评分表3.1.1.1 五子情况
    3.1.1.2 四子情况
    3.1.1.3 三子情况
    3.1.1.4 二子情况
    3.1.1.5 一子情况
    五子棋中有连、长连、五连、成五、四、活四、冲四、死四等诸多概念,可以看到在评分表中有几种没有进行考虑,比如跳活三之类的情况暂时不进行判定。至此,我们有了评分表,接下来我们需要定义评估空位的方法。
    3.1.2 评估函数评估函数是一个对单个可行位置评分的方法,比如它对某个可行位置pos进行评估,评估步骤如下:
    3.1.2.1 横向扫描
    以可行位置pos的左侧为中心,向左扫描如果遇到空格,记录下左侧为空格,停止向左扫描如果遇到己方棋子,棋子个数加1,继续向左扫描如果遇到对方棋子,记录下左侧为对方棋子,停止向左扫描如果已到达最左侧,记录下左侧为墙,停止向左扫描
    以可行位置pos为中心,向右扫描如果遇到空格,记录下右侧为空格,停止向右扫描如果遇到己方棋子,棋子个数加1,继续向右扫描如果遇到对方棋子,记录下右侧为对方棋子,停止向右扫描如果已到达最右侧,记录下右侧为墙,停止向右扫描
    根据棋子个数、评分表为该位置打分,该空位得分score1

    3.1.2.2 纵向扫描扫描纵向上的相连己方棋子个数,根据棋子个数、评分表为该位置打分,该空位得分score2
    3.1.2.3 正斜向扫描扫描正斜向上的相连己方棋子个数,根据棋子个数、评分表为该位置打分,该空位得分score3
    3.1.2.4 反斜向扫描扫描反斜向上的相连己方棋子个数,根据棋子个数、评分表为该位置打分,该空位得分score4
    3.1.2.5 对以上四个方位之后可以得到score=score1+score2+score3+score4,我们将总得分作为该空位的评分,这样就能使AI考虑到周围四个方向。3.1.3 算法效果AI下的位置就是在所有可行位置中评分结果最高的位置,评分结果最高意味着AI觉得这个位置对自己是最有利的,这个算法效果怎么样呢? AI是白棋,玩家先手,由图,AI的第6手是个败笔,AI第6手应该封堵黑棋,但是它并没有这个意识,可知此时的AI太脆弱,不具备”防”的意识,只有“攻”的意识。原始算法的效果如图7所示。

    3.1.4 AI落子过程分析以下通过模拟AI分析棋局的方式分析AI第6手为什么不封堵黑棋,并找到改善的方案。

    玩家执黑,AI执白,首先根据局势找出AI所有可行棋位置


    对上步分析到的可行位置依次计算空位得分,评分结果如下

    单元格中的分数代表该空位的评分,第五行第三列的空位评分最高,所以AI应该将棋子下到该位置处。通过模拟这个过程可以知道,评估函数在为空位打分时只考虑到了己方的棋子,在比对评分表时并没有考虑对方的局势,所以AI决策出的棋子位置只注重了“攻”,而忽视了“防”。
    那么怎么才能平衡两者呢?我想到的方法是“换位思考”,所谓“换位思考”,就是当评估函数对空位打分时,不仅要考虑自己,还要站在对方的角度来看待局势。原算法对空位评分时是以AI白棋的角度评定该空位四个方向上形成的棋型,根据相应棋型给出相应得分aiScore,改进的算法还要以玩家黑棋的角度对空位评分,计算出得分playerScore,该空位的得分为score=aiScore+playerScore。
    3.1.5 算法改进后效果假设刚才的棋局进行到了第6手,轮到AI执棋,我们用改进的算法分析一下AI应该落子的位置

    可知,此时第3行3列和3行7列的单元格评分最高,所以AI会在两个位置上随机选取一个,不管AI选择哪个位置,都将有效的阻止黑棋的进攻,所以说,目前AI已经具备了攻击和防守的能力。
    3.2 关键功能实现3.2.1 窗口布局窗口布局由View类来控制,主要分为两部分:顶部的工具条、中央的棋盘面板。工具条有两个Action:重开一局、玩家先手,分别设置棋盘初始化以及哪位玩家先手,窗口布局如图8所示。

    3.2.1.1 添加工具条以及棋盘//初始化棋盘面板以及添加chessPanel = new ChessPanel();frame.add(chessPanel);// 顶部工具栏JToolBar bar = new JToolBar(); //创建工具栏frame.add(bar, BorderLayout.NORTH); //添加bar.setBorderPainted(false); //设置工具栏不画边框
    3.2.1.2 添加Action至工具条Icon icon = new ImageIcon(View.class.getResource("/image/restart.png")); //IconJButton restartAction = new JButton("重开一局", icon); //ActionrestartAction.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { restartBoard();//重开棋局 }});bar.add(restartAction);//添加Action
    3.2.1.3 鼠标事件//为棋盘面板设置鼠标监听事件chessPanel.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { showChess(chessPanel, e); }});
    在showChess方法中我们要执行的是以下几件事情:

    根据鼠标点击位置计算出棋盘上的行列值
    调用chess对象使玩家在逻辑上落子,调用chessPanel使玩家在视图上落子
    chess对棋盘扫描判定玩家是否获胜,如果获胜则提示并重开棋局,如果未获胜,继续执行
    chess对棋盘扫描并对每个可行的空位评分,选取一个得分最高的位置,调用chess对象使AI在逻辑上落子,调用chessPanel使AI在视图上落子
    chess对棋盘扫描判定AI是否获胜,如果获胜则提示并重开棋局,如果未获胜,继续执行

    之后继续由玩家点击鼠标,回到第一步。
    3.2.2 控制器的实现控制器是整个程序最为核心的一个类,它有多达15个方法,实现了对整个棋局逻辑的控制以及分析。
    以3.1的实现原理为依据,AI分析局势先调用getAllMayLocation()方法获取可行的位置集合,然后对每个空位调用getScore(int x, int y)方法打分,打分的时候需要考虑四个方向以及自己、对方两个角色,打分的步骤是先模拟落子,然后以该点为中心进行四个方向上的扫描,获取每个方向上的棋型,然后调用getScoreBySituation(int count1, int leftStatus, int rightStatus)方法打分,这个方法就是评分表,它对应着3.1中实现原理的评分表。评分表的好与坏直接关系着AI的智能性,这里我只是大概给了评分表的值,细节没有再调。
    以下给出一个局势评估的函数,更多的代码见附录。
    //局势评估函数,评估该点的得分public int getScore(int x, int y) { //使用换位思考思想 //以己方棋子和对方棋子模拟落子计算分数和 int xScore = getXScore(x, y, 1) + getXScore(x, y, 2); int yScore = getYScore(x, y, 1) + getYScore(x, y, 2); int skewScore1 = getSkewScore1(x, y, 1) + getSkewScore1(x, y, 2); int skewScore2 = getSkewScore2(x, y, 1) + getSkewScore2(x, y, 2); return xScore + yScore + skewScore1 + skewScore2;}
    3.2.3 棋盘的实现棋盘的绘制与棋子的绘制都比较简单,为了实现落子操作,我们需要使用一个集合保存棋盘的状态,用Loction保存每个位置的信息,重绘时遍历集合绘制棋子。比较麻烦的一点是绘制棋子上边的数字,这个数字是用来记录棋子是第几手下的,棋盘上有黑棋、白棋,那么棋子上的数字颜色也应该是两种,以下代码中string就是要画的字符串,核心代码如下:
    FontMetrics metrics=g.getFontMetrics();int ascent = metrics.getAscent();int descent = metrics.getDescent();if(location.getPlayer()==Chess.first) g.setColor(Color.white);else g.setColor(Color.black); //设置棋子颜色g.drawString(string,margin + location.getY() * row-metrics.stringWidth(string)/2,margin + location.getX() * row-(ascent+descent)/2+ascent);
    3.2.4 位置实体类实现这个类是个实体类,只有四个属性以及setter、getter方法以及两参、三参、四参构造方法,封装的是一个位置实体类。
    4 运行结果4.1 效果图人获胜如图9所示,机器获胜如图10所示。

    4.2 问题分析出现的问题主要有三个:

    问题一:评分表的值怎么设定
    问题二:机器先手这个功能怎么实现
    问题三:棋子上的数字字符串怎么居中

    对于问题一:我觉得这个是靠经验吧,我先根据各个棋型的重要性给一个大致的分数,然后再慢慢调试,这样的结果可能不是最优的,但是基本可以用了。
    对于问题二:机器先手是指机器需要先决策第一手棋的位置,所以Chess需要提供一个start()方法返回第一个位置,因为此时棋盘上没有棋子,所以AI分析后也不会有结果,我就直接把中心的点给返回了作为AI第一步棋的位置。
    对于问题三:首先在绘制棋子的时候我们可以知道棋子要绘制在第几行第几列,然后根据这个行列值计算出棋子所在的圆心位置,字符串的横坐标=圆心的横坐标-字符串的宽度/2,字符串的高度=圆心的纵坐标-字符串的高度/2。

    横坐标=圆心横坐标-metrics.stringWidth(string)/2纵坐标=圆心纵坐标-(ascent+descent)/2+ascent
    5 参考文献[1] 李刚,疯狂Java讲义第3版,北京:电子工业出版社,2014.7
    [2] 郑莉,Java语言程序设计第2版,北京:清华大学出版社,2011.6
    1 评论 29 下载 2018-11-03 13:47:19 下载需要1点积分
  • 基于JAVA的即时通信软件

    一.设计任务书1.1 设计任务本文设计的是一个简单的即时通信软件,利用 Java Socket 进行点到点通信,其工作机制模仿即时通信软件的基本功能,已实现的功能有:

    客户端登录客户端退出群组成员之间传输文字或图片信息
    该软件分为客户端与服务器端,客户端负责与服务器建立连接,且执行收发消息的操作,服务器端负责等待客户端连接并保存用户的昵称与客户端 Socket 的输出流的对应关系。
    1.2 技术指标本程序使用的是 TCP 协议实现的即时通信软件,程序是基于 Java 语言开发的,主要用到的技术有:

    Socket 编程自定义协议
    如果使用普通的方法来标记一条消息的结束,如换行符,那么程序就不易扩展,只能发送纯文本消息,所以需要自己定义一种消息的格式,并且我们还需要提供发送消息与解析消息的方法。
    服务器端创建一个 ServerSocket,循环等待客户端的连接,每当有客户端连接,就获取到客户端的 Socket 对象,并将该对象交付给一个服务器端线程处理,服务器端线程会不断从 Socket 的输入流中解析出消息类型、长度及消息内容,然后根据类型执行不同的操作。
    客户端与服务器建立连接,同时开启一个客户端线程接收服务器端发送的消息,客户端登录是向服务器发送一条登录命令,客户端向服务器发送一条消息首先需要包装成定义的消息格式,然后再发送给服务器。
    不管是发送消息还是发送命令其实本质都是一条消息,向服务器发送的消息都必须按照定义的格式来。
    1.3 论证结果经论证,这个任务是可行的。TCP 协议的实现细节 Java Socket 已经帮我们做好了,我们需要做的是定义一个协议工具类,实现发送消息与接收消息的方法,然后客户端与服务器端都利用这两个方法来进行消息的发送与解析。
    二.实现原理2.1 基于 TCP 协议的即时通信TCP 协议是一种端到端协议,当一台计算机要与远程的另一台计算机连接时,TCP 协议会让他们建立一个用于发送和接收数据的虚拟链路。TCP 要负责收集数据信息包,并将其按照适当的次序放好传送,接收端收到后再正确的还原,TCP协议使用了重发机制,当一个通信实体发送一个消息到另一个通信实体后,需要接收到另一个通信实体的确认消息,如果没有收到确认消息,则会重发消息。所以 TCP 协议保证了数据包在传输中不发生错误。通信示意图如图 1 所示。

    在通信实体 1 与通信实体 2 建立虚拟链路前,必须有一方主动来接收其他通信实体的连接请求,作出“主动”的通信实体称为服务器,发出连接请求的通信实体称为客户机。
    2.2 自定义协议的定义2.2.1 通信原理客户端与服务器端相互通信,首先要建立 Socket 连接,连接建立好后双方都会拿到一个 Socket 对象,通过 Socket 对象拿到输入、输出流可以实现写、读的功能。服务器端接收到客户端的连接,将客户端的 Socket 对象交付给一个线程,该线程负责维护该客户端,在线程体中需要使用死循环不断的获取客户端发给服务器的消息。
    2.2.2 存在的问题那么问题来了:怎么标志客户端发送的消息的结尾?如果不对结尾标志,服务器端将不知道客户端本次客户端发送的消息到哪里。
    2.2.3 文本消息的解决办法对文本消息的一般做法是将‘\n’作为结尾标记,操作过程如下:

    客户端与服务器端建立连接,服务器端将客户端的 Socket 加入集合中保存,并将客户端的 Socket 交付给一个服务器端线程处理,服务器线程初始化套接字、输入输出流,然后一直循环等待客户端发送消息
    客户端向服务器发送消息“Hello World!\n”,服务器线程获取到客户端发送的消息,然后使用输入流读取一行消息,读取到的消息是“Hello World!”,然后遍历服务器端的那个集合,获取到集合中每个 Socket 的输出流,向每个输出流中写入消息。

    以上是一般意义上群聊的实现原理:客户端向服务器发送消息,服务器获取到消息后转发给群组中的所有成员。
    2.2.4 依然存在的问题
    问题一:如果发送的是图片、文档应该怎么标记消息的结束?问题二:在实际应用中,客户端向服务器端发送消息并不像刚才的例子那么简单,还需要登录、注销、登录成功等命令,怎么来区别这些命令?
    2.2.5 自定义协议的内容为解决以上问题,我们规定:消息的发送与解析都必须使用以下格式:

    由表 1 知,数据分为三个部分

    type:1 字节,表示发送的消息类型,所以可以表示 65535 种消息类型。totalLen:4 字节,整型数据,表示发送的消息的总长度,包含 type、totalLen的长度以及消息内容的长度,totalLen 占用 4 字节,所以最大可以发送 2G 的数据。bytes[]:字节数组,表示发送的消息的内容,大小没有限制。
    2.2.6 使用自定义协议制定消息的规范后以上两个问题都会迎刃而解了,客户端向服务器端发送消息的过程如下:
    例 1:发送纯文本
    客户端:

    客户端的视图与用户交互获取到用户要发送的文本内容“你好啊”客户端将获取到的文本内容转化为字节数组 bytes客户端将消息包装成自定义的消息格式,如表 2 所示


    客户端往输出流中写入消息
    服务器端:

    服务器端线程一直等待接收客户端的消息服务器端线程获取到客户端发送的消息,按照格式解析出消息的类型、长度以及消息内容服务器端线程获取到的消息类型是文本类型 TYPE_TEXT,那么需要遍历服务器端的集合,获取到集合中每个 Socket 的输出流,使用这个输出流对消息转发,在转发前同样需要包装成定义的消息格式。
    例 2:登录功能
    客户端:

    客户端与服务器端建立连接之后,将用户输入的昵称包装成一条消息,如表 3 所示,消息类型是 TYPE_LOAD,字节数组 bytes 是用户昵称。


    在建立连接的同时客户端会开启一个线程等待接收服务器端的消息。将消息发给服务器端。客户端线程如果收到服务器端反馈的信息,就将信息告知用户。
    服务器端:

    接收到消息后获取到消息类型为 TYPE_LOAD,服务器端就可以知道这条消息是登录请求,然后 bytes[]数组里的数据就是用户昵称,将用户昵称、该客户端的 Socket 对象的输出流先保存到 Map 中,然后将该 Map 保存到集合中。服务器端线程对客户端的登录请求处理完成后,向客户端反馈一条标记着是否操作成功的消息,类型是 TYPE_LOADSUCCESS 和 TYPE_LOADFAIL,bytes 数组是服务器端反馈的消息。
    如果登录成功,服务器反馈的消息格式如表 4 所示。

    如果登录失败,服务器反馈的消息格式如表 5。

    2.2.7 小结其实不管客户端发送的消息是哪种类型,客户端只需要负责将要发送的消息转化为字节数组,然后对数据包装后发送给服务器。对于一些非命令类的消息,服务器接收到消息后只需要根据数据类型对数据进行解析、包装、转发即可。对于一些命令类的消息,如登录,退出等功能,则需要服务器端执行相应的操作,服务器端不需要对消息转发,可能需要对一些命令给予反馈。
    通过自定义协议可以解决上述的两个问题,并给出了客户端与服务器端使用自定义协议发送消息的两个详细步骤
    2.3 自定义协议的实现这个自定义协议就是自己定义的一个发送消息、解析消息的规范,无论是发消息还是收消息都必须按照这个规范来,实现这个协议无非需要考虑三个问题:

    问题一:如何发送消息?问题二:如何解析消息?问题三:如何表示解析消息后的结果?
    我们只需要定义两个类,协议工具类 Protocol 负责消息的发送与解析,消息结果类 Result 封装了一个消息的三个部分:type、totalLen、bytes,协议工具类对消息解析后会返回一个 Result 对象表示一次解析的结果。所以这两个类结合起来使用就可以解决以上三个问题。
    2.3.1 协议工具类的实现协议工具类 Protocol 负责消息的发送与解析,内部需要定义消息的格式,协议工具类的设计如图 2 所示。

    2.3.1.1 消息类型消息类型是协议工具类的静态的公共的整型常量,这样的设置为程序后期的扩展提供了方便,提供的消息类型如表 6 所示。

    2.3.1.2 发送消息发送消息就是按照定义的格式往输出流中写入数据,我们首先要做的是包装数据定义方法签名,如表 7 所示。


    包装数据
    一条数据有三部分,消息类型、消息内容已经通过参数获取到了,消息的长度还要程序计算:消息长度=消息的内容的长度+5 字节。
    int totalLen = 1 + 4 + bytes.length;

    按格式写入输出流,先写入消息类型,然后写入消息的总长度,最后再写入消息的内容。
    dos.writeByte(type);dos.writeInt(totalLen);dos.write(bytes);dos.flush();
    2.3.1.3 解析消息解析消息是指将从输入流中读取到一条消息,然后按照格式转化为一个结果对象 Result。定义方法签名如表 8 所示。


    消息提取
    从输入流中依次读取三部分:type、totalLen、bytes[],dis 是方法的参数,调用方法时需要传入输入流
    byte type = dis.readByte();int totalLen = dis.readInt();byte[] bytes = new byte[totalLen - 4 - 1];dis.readFully(bytes);

    结果返回
    将提取出来的数据的三个部分封装成一个结果对象作为方法的返回值,注意第一个参数:type & 0xFF,因为 type 是字节,需要与 0xFF 进行“与”运算得到一个整型值。
    return new Result(type & 0xFF, totalLen, bytes);
    2.3.2 结果类的实现结果类 Result 封装一条消息的三个部分,主要提供了 setter、getter 方法来设置或者获取消息的三个组成部分,结果类的设计如图 3 所示。

    2.3.2.1 消息格式Result 类定义了消息的格式,消息的组成如表 9 所示。

    2.3.2.2 方法Result 类的方法签名如表 10 所示。

    2.4 服务器端的实现2.4.1 服务器类Server 类负责等待客户端连接并将连接上的客户端交付给服务器线程类。Server 类的设计如图 4 所示。

    clients 维护一个 List 集合,集合中每个元素都是 Map,每个 Map 中都有两个 item,保存着客户端的昵称和对应的输出流。
    main 方法中要实现的是等待客户端连接,使用 ServerSocket,有客户端连接时开启一个线程来处理。代码如下:
    ServerSocket serverSocket = new ServerSocket(30000);while (true) { Socket socket = serverSocket.accept(); new Thread(new ServerThread(socket)).start();}
    2.4.2 服务器线程类ServerThread 类负责接收客户端的消息并对消息进行相应的处理,ServerThread 类的设计如图 5 所示。

    2.4.2.1 变量ServerThread 类的变量以及其含义如表 11 所示。

    2.4.2.2 方法签名ServerThread 类的方法签名以及含义如表 12 所示。

    2.5 客户端的实现2.5.1 客户端界面界面的元素有:登录输入框、聊天内容文本域、消息输入文本域、发送按钮。客户端界面初始化时会调用 Client 的方法执行客户端与服务器的连接请求,连接成功后客户端与服务器端会形成一个虚拟链路,当用户输入用户名后回车,客户端通过该虚拟链路向服务器端发送一条登录命令。View 类的设计如图 6 所示。

    2.5.2 客户端客户端 Client 负责处理客户端连接、客户端发送消息的任务,Client 类的设计如图 7 所示。

    2.5.2.1 建立连接socket = new Socket(address, port);dos = new DataOutputStream(socket.getOutputStream());// 监听服务器发回的消息new ClientThread(socket).start();
    2.5.2.2 登录public void load(String user) { Protocol.send(Protocol.TYPE_LOAD,user.getBytes(), dos);}
    2.5.2.3 发送消息public void sendMsg(String msg) { Protocol.send(Protocol.TYPE_TEXT, msg.getBytes(), dos);}
    2.5.2.4 退出public void logout(){ Protocol.send(Protocol.TYPE_LOGOUT, "logout".getBytes(),dos);}
    客户端线程负责接收服务器端发的消息,其对消息的处理方法与服务器端线程的处理方法类似,都是先解析消息,然后根据消息类型执行相应的操作。
    2.5.2 客户端线程客户端线程主要负责接收消息,并对接收到的消息进行显示。ClientThread类的设计如图 8 所示。

    ClientThread 类维护的是客户端的套接字以及输入流,那三个方法的作用和服务器端线程类似,这里不再细说。
    三.实验结果3.1 运行结果
    服务器端启动后是没有运行界面的,运行结果如图 9 所示。


    客户端启动后初始界面如图 10 所示。


    输入用户名后回车登录,登录后如图 11 所示。


    两个客户端互发消息,如图 12 所示。


    单个客户端退出

    3.2 主要问题及故障分析3.2.1 主要问题
    不知道如何标记一条消息的结尾界面问题
    3.2.2 故障分析对于第一个问题:如果只是发送一条文本消息的话,是没有这个问题的。但是为了使程序拥有更好的扩展性,使其可以发送图片以及文档,这个问题还是值得思考的。定义一种消息的格式,无论是发送还是接收消息都按照这个标准来,这个就是我们定义的协议工具类的作用。
    对于第二个问题:本程序是基于 Java 语言开发的,AWT 和 SWING 是 Java 语言开发 GUI 的工具包。SWING 和 AWT 写界面都不是很方便,所以本程序的界面有点粗糙。
    3.3 设计结论由于之前写过这类的程序,所以在程序层次上的实现并不难,本次实验不仅巩固了编写程序的功底,还加深了对 Socket 通信底层理论的理解,可以说是收获非常大。至此,本论文已经接近尾声,所研究的是一个简单的即时通信软件的实现过程以及实现原理。
    四.附录一:实验相关4.1 实验数据客户端与客户端 2 发送的消息数据,[]内部表示的是发送方的昵称,昵称外部是发送的消息内容,具体实验数据如下:

    [客户端 1]我是客户端 1[客户端 1]你好啊![客户端 2]我是客户端 2
    4.2 系统软硬件环境4.2.1 硬件环境
    系统:Window7 旗舰版系统类型:64 位操作系统处理器:i5-4210U安装内存:4.0GB
    4.2.2 软件环境已安装 JRE、JDK 并配置好环境变量
    4.3 使用说明本程序分为客户端与服务器端,首先需要启动服务器端,然后可以打开多个客户端,客户端打开后可以进行聊天。
    4.4 参考资料[1] 李刚,疯狂 Java 讲义第 3 版,北京:电子工业出版社,2014.7
    [2] James F.Kurose,Keith W.Ross,计算机网络-自顶向下方法上册(第 5 版),北京:高等教育出版社
    1 评论 80 下载 2018-11-03 13:32:16 下载需要2点积分
  • 基于JAVA的电梯调度模拟

    一、项目要求概述1.1 项目目的
    通过控制电梯调度,实现操作系统调度过程学习特定环境下多线程编程的方法学习调度算法
    1.2 开发环境
    语言:java系统平台:全平台(具备java环境)IDE:Intellij IDEA产品呈现模式:jar包执行环境要求:安装java
    Win:安装java配置环境变量后双击Linux/Mac:命令行:java –jar 电梯.jar

    1.3 基本需求
    模拟20层楼中5架电梯的调度电梯具有最基本的按键可显示电梯的当前状态
    二、调度算法概述2.1 乘客行为概述
    乘客可以在20层楼的任何一层楼按当前楼层的上或者下的按键对电梯提出需求乘客可以按动电梯中的楼层选择按钮来对指定电梯前去哪里,由于ui的设计问题,这一功能被要求在按动请求按钮时一并完成乘客可以在电梯中按动紧急按钮迫使当前电梯停止运作
    2.2 电梯行为概述
    电梯初始状态均为静止,且停泊在第一层电梯通过反复自检自身的状态变量来变更自己的行为行进中的电梯每到一个楼层都自检下客队列,判断当前楼层是否需要开门下客行进中的电梯每到一个楼层都要检查当前楼层乘客等待队列是否有符合当前方向的乘客,判断当前楼层是否要载客,如果在该楼层电梯中没有了乘客且没有应答其他请求,则载上当前楼层人数较多方向的乘客继续行进
    2.3 调度
    乘客按下请求按钮响应流程
    上下方向上有朝这一楼行进且该电梯的最高/低请求大于该楼层:将会等待该电梯到达该楼层来载上该乘客上下方向上没有朝这一楼行进的电梯或是有但是该电梯最高/低请求并没能到达该楼层:将会进行检索静止的电梯队列:静止电梯的选择将位置优先,选择离该楼层最近的静止电梯来响应请求,将该电梯启动,并将在该楼停下的指令塞入该电梯。


    2.3.1 行进电梯到达某一楼层执行操作流程
    电梯检索自身的停止队列中是否有该楼层
    有该楼层:停留并将队列中的乘客全部弹出队列,将停止队列的该楼层弹出没有该楼层:进行下一步
    电梯检索当前楼层的请求队列
    电梯当前停止队列已经为空:
    当前楼层没有请求电梯设置自身状态变量为静止当前楼层有请求电梯选择人多的一个方向载客,将他们弹出请求队列,并设置状态变量然后启动向该方向行进
    电梯当前停止队列并未空
    当前楼层没有请求电梯继续行进当前楼层有请求电梯载上对应方向的乘客,将这些乘客从请求队列弹出,继续行进



    三、类概述
    私有变量
    int name; // 电梯名int currentState; // 当前状态变量int emerState; // 紧急状态变量int currentMaxFloor; // 当前可去的最高的楼层int maxUp; // 当前电梯要去的最高楼层int minDown; // 当前电梯要去的最低楼层Queue<Integer> upStopList; // 电梯下降停止队列Queue<Integer> downStopList; // 电梯上升停止队列JButton buttonList; // ui中的按钮控件队列
    方法
    int getCurrentState(); // 获取currentStatevoid setCurrentState(); // 设置currentStateint getCurrentFloor(); // 获取currentFloorvoid setCurrentFloor(); // 设置currentFloorvoid popUp(); // 将upStopList的第一个元素弹出void popDown(); // 将downStopList的第一个元素弹出void addUp(int pos); // 将位置楼层加入upStopListvoid addDown(int pos); // 将位置楼层加入downStopListint upMax(); // 获取maxUpvoid setUpMax(); // 设置maxUpint downMin(); // 获取minDownvoid setDownMin(); // 设置minDownvoid run(); // 启动电梯线程
    四、线程概述4.1 资源
    电梯
    4.2 任务
    乘客移动
    1 评论 56 下载 2018-11-03 13:20:28 下载需要5点积分
  • 基于汇编语言实现打字练习软件

    一 需求分析根据以下几部分来实现打字练习:

    随机显示字母,字母出现的位置随机
    字母自动落下
    从键盘输入的字母与落下字母相同则该字母消失,否则字母自动接着落下
    按下“Esc”键则程序返回主菜单
    字母下落过程中按空格键暂停
    在主界面按“E”则程序退出

    打字练习的主要功能由以上六部分组成,每一部分之间的联系都是比较紧密的。对于以上及部分,最主要的部分就是中间的四个部分,这是打字练习的重点,需要详细设计其所需要的功能。
    二 程序设计主模块是打字游戏的核心模块,主要通过各个键盘符来控制各个子模块之间的协调,完成打字游戏的运行。
    子模块主要包括:初始化子模块、速度设定子模块、显示时钟子模块、开始打字子模块,显示打字结果子模块。

    初始化子模块包括显示初始界面菜单,初始化程序参数,判断是否进入游戏
    速度设定子模块包括速度选择子程序和速度设置子程序
    显示时钟子模块包括取系统时钟和显示两个子程序
    开始打字子模块包括显示分数子程序,当敲入字符与下落相符时扬声器发声子程序,字母下落子程序,产生新的字母和新的位置子程序,延时子程序。这些程序有机的组合在一起,完成整个指法练习的程序

    初始化子模块包括初始化程序参数,显示初始界面菜单,判断是否进入游戏。首先初始化字母出现的位置,初始化得分和各种标志的值,然后显示初始界面菜单,通过一个比较指令和堆栈操作来判断是否进入游戏。
    2.1 系统总体框架
    2.2 系统流程图
    三 程序实现3.1 实现环境
    硬件环境:IBM-PC机,硬盘40G以上,内存256M以上,打印机等
    软件环境:Windows 2000 Server或Windows XPServer操作系统,TC,QE等编辑软件,MASM汇编软件

    3.2 关键代码说明Init_game macro op1,op2,op3,op4,op5 local ns mov cx,00h mov dh,op1 mov dl,op2 ns:mov ah,02h;设置光标位置 mov bh,00h;页号为0 int 10h push cx mov ah,0ah;在当前光标位置写字符 mov al,op3;al=字符的ascii码 mov bh,00h;bh=页号bl=字符属性 mov cx,01h;cx=写入字符重复次数 int 10h pop cx;cx=0 inc cx;cx=cx+1 inc op4 cmp cx,op5 jne ns endmclear_screen macro op1,op2,op3,op4 ;清屏宏定义 cx,屏幕的左上角,dx屏幕的右下角 mov ah,06h mov al,00h mov bh,0eh;改变行属性的色彩,字的色彩,bh空白行的属性/07就是正常的黑底白字 mov ch,op1 mov cl,op2 mov dh,op3 mov dl,op4 int 10h mov ah,02h;设置光标的位置从0000开始 mov bh,00h mov dh,00h mov dl,00h int 10hendmmenu macro op1,op2,op3 ;菜单显示宏定义 mov ah,02h mov bh,00h mov dh,op1 mov dl,op2 int 10h mov ah,09h lea dx,op3 int 21hendmdata segment ZK db "WELCOME TO PLAY$" no db "date:2013/12/27$" meg db "press Enter key to continue.......$" meg1 db "when a letter is dropping,please hit it!$" meg2 db "press space key to pause!$" meg3 db "press ESC key to return main interface!$" meg4 db "press letter 'E' to exit!$" speed dw 600d letters_bak db "jwmilzoeucgpravskntxhdyqfb" db "iytpkwnxlsvxrmofzhgaebudjq" db "nwimzoexrphysfqtvdcgljukda" letters db 78d dup(0) letter_counter db 0 life_flag db 78 dup(0) position_flag db 78 dup(0) present_position db 1 data endsstack segment para stack 'stack' db 64 dup(0)stack endscode segment main proc far assume cs:code,ds:data,ss:stack start: mov ax,data mov ds,ax mov letter_counter,00h mov present_position,1 lea si,position_flag; mov ah,00h mov cx,00h lea di,letters;di的偏移地址为letters lea si,letters_bak;si的偏移地址为letter_bak mov cx,00h;cx=0 init_letters: mov ah,[si];ah=j mov [di],ah;ah的值放到letters里面;letters_bak的值放入letters里面 inc si;si+1 inc di;di+1 inc cx;cx+1 cmp cx,78d; jne init_letters;不为0就到init_letters,一直循环到letters里 mov ah,00h lea si,life_flag; mov cx,00h init_life_flag: mov [si],ah inc si inc cx cmp cx,78d jne init_life_flag mov cx,00h ;ch=光标开始行,cl=光标结束行 根据CX给出光标的大小 mov ah,01h or ch,00010000b;ch>20h,光标消失,cl>20h,覆盖字符 int 10h clear_screen 00d,00d,24d,79d ;清屏,0000- 2479 Init_game 00d,00d,0ah,dl,80d ;这个四个是初始化屏幕的上下左右的框框 Init_game 24d,00d,0ah,dl,80d Init_game 00d,00d,0ah,dh,25d Init_game 00d,79d,0ah,dh,25d menu 05d,15d,ZK ;菜单信息的宏调用,这五行是在屏幕上显示提示消息 menu 07h,15d,no menu 09d,15d,meg menu 11d,15d,meg1 menu 13d,15d,meg2 menu 15d,15d,meg3 menu 17d,15d,meg4 put: mov ah,02h ;设置光标位置 mov bh,00h;设置页码 mov dh,22d;dx行列坐标 mov dl,33d int 10h mov ah,01h ;从键盘输入任意字符并回显示,al=输入字符 int 21h cmp al,0dh;是否为换行符 je speed3;如果是换行符则跳转到speed3处 cmp al,45h;比较是否为e je exit;如果为e,转到exit exit: mov ah,4ch int 21h speed3: mov ax,speed+12 mov speed,ax jmp begin begin: clear_screen 01d,01d,23d,78d ;清屏宏调用 ; clear_screen 01d,01d,23d,78d Init_game 23d,01d,03h,dl,78d;23d01d行列坐标,初始化倒数第二行的字符 mov ah,02h mov bh,00h mov dh,01h mov dl,01h int 10h mov cx,00h lea si,letters ;si的偏移地址是letters nextletter: mov ah,02h ;显示字母 mov dl,[si] ;把letters的字符放到dl里 int 21h ;通过dos中断的2号功能项,把字符显示出来 inc si inc cx cmp cx,78d je nextcycle;全部显示完了后,跳到nextcycle jmp nextletter from_front: sub present_position,78d ;当超过78个字时的处理方式 减去78 jmp gobackto_si;跑到gobackto_si这来 find_zero: cmp letter_counter,78d ;letter_counter有78了,初始化 je recycle;如果有跑到recycle cmp present_position,78d;如果present_position等于78d, je from_one mov ah,00h nextsi: add present_position,01h inc si cmp [si],ah je gobackto_di cmp present_position,78d je from_one jmp nextsi from_one:mov present_position,01h ;present_position=01 jmp gobackto_si recycle:mov letter_counter,00h;letter_counter=0 mov present_position,01d;present_position=01 lea si,position_flag;si=position_flag的偏移地址 mov cx,00h mov ah,00h clearsi: mov [si],ah;position_flag地址搞成0 inc cx cmp cx,78d je nextcycle inc si jmp clearsi nextcycle: lea di,letters;di的偏移地址是letters[字母] lea si,position_flag;si的偏移地址是position_flag add present_position,31d;31一跳,这个你可以随便设置 cmp present_position,78d;;超过78个字节 ja from_front gobackto_si: add si,word ptr present_position;si=si+present_position,si向后偏移 dec si; 要不要都无所谓,只不过,因为开始就觉定了是要31一跳,所以这里减一个1位 mov ah,[si];把position_flag放到ah里 cmp ah,01h;看看position_flag里面有没有标志1 je find_zero;如果ah为1转移,否则 gobackto_di: mov ah,01h mov [si],ah add di,word ptr present_position dec di;因为列坐标是从0开始,而字符是从1开始,所以这里是32-1 mov dl,present_position; mov ah,02h mov bh,00h mov dh,01h int 10h mov cx,00h nextrow: push cx mov cx,00h out_cycle: ; 延迟 push cx mov cx,00h in_cycle: add cx,01h cmp cx,1000 ; jne in_cycle ;zf=0转到标号处执行, push dx mov ah,06h ;从键盘输入字符,al等于字符 mov dl,0ffh int 21h pop dx jz pass cmp al,1bh ;如果键入ESC,则返回主菜单 je to_start1 cmp al," " ;如果键入SPACE,则游戏暂停 je pause cmp al,[di] ;输入字母正确!则字母消失 je disappear pass: pop cx inc cx cmp cx,speed je print jmp out_cycle pause: push dx ;暂停处理 第一次知道暂停是这样的,循环空代码 mov ah,06h mov dl,0ffh int 21h pop dx cmp al," " jne pause jmp pass to_start1: ;返回主菜单 jmp start print: mov ah,0ah ;在当前光标位置写空格 mov al," " mov bh,00h mov cx,01h int 10h inc dh mov ah,02h ;改变光标位置 mov bh,00h int 10h mov ah,0ah ;在当前光标位置写字母 mov al,[di] mov bh,00h mov cx,01h int 10h pop cx inc cx cmp cx,21d je print_next_letter jmp nextrow ;下一行 disappear: ;击中字母后输出空格 pop cx pop cx mov ah,0ah;在光标处按原来属性显示字符 mov al," " mov bh,00h mov cx,01h int 10h jmp hit print_next_letter: lea si,life_flag add si,word ptr present_position dec si mov ah,0ah;在当前光标处按原有属性显示字符 mov al," ";最倒数第二排写入字符,意思是当掉下来的字符到倒数第二行的时候,自动变成空格消失 mov bh,00h mov cx,01h int 10h inc dh ;这就是到了最后一行 mov ah,02h;2号中断,设置文本光标位置 mov bh,00h int 10h mov ah,0ah;把最后一行的字符变成空格 mov al," " mov bh,00h mov cx,01h;重复输出,这里的重复输出的意思就是输入一个空格 int 10h mov ah,1;把life_flag变成1,这样下次就可以不在同一个位置掉字符下来 mov [si],ah hit: mov ah,02h;设置光标 mov bh,00h mov dh,01h;第一行 mov dl,present_position;下一个字符的列 int 10h mov al,[di] ; 出现下一个新字母的数法 add al,7;di+7 cmp al,7ah;z的ascii码就是7ah,所以当al大于7ah时转移 ja convey_letter mov ah,0ah;在当前光标按原有属性显示字符,al=字符 mov bh,00h mov cx,01h int 10h mov [di],al add letter_counter,01h;统计次数 jmp nextcycle convey_letter: sub al,7ah add al,61h;al等于要显示的字符,加61表示是小写字母 mov ah,0ah mov bh,00h mov cx,01h int 10h mov [di],al add letter_counter,01h jmp nextcycle clear_screen 01,01,23,78 mov ah,02h mov bh,00h mov dh,11d mov dl,20d int 10h inc dh inc dh mov ah,02h mov bh,00h int 10h notkey: mov ah,07h int 21h cmp al,0dh je to_start cmp al,1bh je over jmp notkey to_start: clear_screen 00,00,24,79 jmp start over: clear_screen 01,01,23,78 mov ah,02h mov bh,00h mov dh,11d mov dl,15h int 10h mov ah,02h mov bh,00h mov dh,13d mov dl,15h int 10h mov ah,07h int 21h mov ah,07h int 21h clear_screen 00,00,24,79 mov ax,4c00h int 21h main endpcode endsend start四 运行测试


    五 参考文献[1] 方立友.微机原理与汇编语言实用教程.北京:清华大学出版社,2007年02月
    [2] 颜志英.微机系统与汇编语言.北京:机械工业出版社,2007年9月
    [3] 王成.计算机组成原理实验指导书与习题集.北京:清华大学出版社,2000年4月
    [4] 张代远.计算机组成原理教程.北京:清华大学出版社,2005年6月
    [5] 朱家铿.计算机组成原理.沈阳:东北大学出版社,2000年3月
    [6] 王爱英.计算机组织与结构.北京:清华大学出版社,1998年7月
    [7] 唐塑飞.计算机组成原理.北京: 高等教育出版社,2000年9月
    [8] 白英中.计算机组成原理.第二版.北京:科学出版社,2001年3月
    1 评论 13 下载 2018-11-03 10:40:58 下载需要2点积分
  • 基于汇编语言的MVC思想架构2048小游戏

    一 需求分析在Win32环境下,使用MVC思想架构,同时应用多文件多模块的软件设计实践,以MASM6.15为主要汇编工具,Sublime Text 3为代码编写工具,综合利用多种汇编命令语句,进行2048游戏设计开发。
    二 技术路线2.1 系统架构程序分为一个主模块和三个子模块,其中排行榜模块由于时间关系暂未能完全实现,现只能查看最高分。
    程序架构如下图所示:

    2.2 各模块详细设计程序共分为4个文件:main.asm,game.asm, rank.asm, lib.asm。
    main.asm是程序的主模块,程序的主界面、功能选择都在这里实现。流程图如下所示:

    rank.asm实现了存储最高分记录的功能,包括了创建、读写、关闭文件等功能。能够更新最高分。流程图如下所示:

    game.asm是程序的核心模块,实现了整个游戏功能。其中又以GAME函数为主函数,其他如REVIE函数则作为GAME调用的子函数。流程图如下所示:

    lib.asm存储了程序中频繁使用的如输出字符串、清屏、输出回车、得到用户输入等程序段,在其他文件中可以以宏的方式调用这些功能。
    三 程序实现主模块和最高分模块都没有涉及到复杂的算法。主模块主要是输出提示信息和得到用户输入,最高分模块则主要是调用了INT 21H中断中的文件操作功能,在这里就不赘述了,主要说明一下game模块。
    game模块分为两大部分,后台数据处理模块和前端界面输出模块。
    后台数据部分,根据游戏本身的特点,可以不用存储2、4、8……2048……等大数,而是只存储其相对于2的幂次,比如8存储为3,1024存储为10等。这样就可以用长度为16的DB数据段来存储游戏中各方格对应的数据。程序中用MODE数据段来存储,其与游戏中各方格的对应关系为:

    当用户输入一移动指令后,REMOD负责对MODE的更新操作。GAME根据用户输入的方向设置好DI、RECI和RECO(分别代表遍历的起点,内层遍历递增量和外层遍历递增量)后调用REMOD。REMOD会从MODE[DI],以RECO为每次循环的递增量对REMOD进行遍历,每次对MODE[DI]调用一次RELIN子程序对MODE[DI]进行更新。RELIN则会对MODE[DI]之后的方块进行遍历(以RECO为递增量),将其与MODE[DI]进行比较以确定是否要改变两者的值,直至越界或者不可能更新时(两者的值不相同且都不为0)就退出。其流程图如下:

    对MODE更新完毕后会调用PUT1将MODE中随机一个方格中的0变为1,实现随机放置一个2的效果。其实现方式是通过INT 1AH读取时钟计数器作为随机数,对16取模得到一个0~15的数值i,之后将MODE[i]之后最近的(超过15则循环至0)一个值为0的方格置为1。
    PUT1执行后调用REVEW根据MODE中的值查表得2相对于其的幂次的值,查表得到该格子在屏幕中的位置,将光标定位到相应位置后,将要打印的值输出到屏幕。这样将MODE中的值都打印到屏幕中后,跳转至等待用户输入的程序段,等待用户继续输入下一次指令。
    当用户输入的是Q(不能是q)时,程序会输出一段提示字符串和用户游戏的最高分,并调用rank.asm中的SAVSCR函数记录当前分数。之后进入一段空循环,实现延时的功能,最后退出至主页面。
    四 运行测试程序在32位的Windows XP 系统上运行的效果图:






    五 参考文献[1] Muhammad Ali Mazidi. The x86 PC: AssemblyLanguage, Design, and Interfacing, Fifth Edition [M]. 第五版.电子工业出版社, 2008年4月.
    [2] 王成耀. 80x86汇编语言程序设计(第二版)[M]. 第2版.人民邮电出版社, 2011年1月.
    [3] 王晓虹 毕于深 李 飒.汇编语言[M]. 第1版.清华大学出版社, 2011年4月.
    1 评论 33 下载 2018-11-03 09:27:37 下载需要4点积分
  • 基于UNIX V6++设计的二级文件系统

    一、课程设计基础任务描述为 LINUX 设计一个简单的二级文件系统。本实验用某个大文件,如 c:\myDisk.img , 存储整个文件卷中的所有信息。一个文件卷实际上就是一张逻辑磁块盘,磁盘中存储的信息以块为单位。每块 512 字节。 复习并深入领会 UNIX V6 文件管理系统的内核设计思想。 要求做到以下几点:
    可以实现下列基础 API void ls();
    列目录
    Int fopen(char *name, int mode); Void fclose(int fd); Int fread(int fd, char *buffer, int length); Int fwrite(int fd, char *buffer, int length); Int flseek(int fd, int position); Int fcreat(char *name, int mode); Int fdelete(char *name)
    同时做到创建目录,进入目录等简单的辅助功能,同样对应三个 API:
    Void mkdir(char* dirname); Void cd(char* dirname); Void backDir()
    二、设计思想说明2.1 任务分析一个文件系统从功能上划分程序为四个部分:

    第一部分是有关高速缓冲区的管理程序,主要实现了对硬盘等块设备进行数据高速存取的函数
    第二部分代码描述了文件系统的底层通用函数,说明了文件索引节点的管理、磁盘数据块的分配和释放以及文件名与 i 节点的转换算法
    第三部分程序是有关对文件中数据进行读写操作,包括对字符设备、管道、块读写文件中数据的访问
    第四部分的程序与文件的系统调用接口的实现有关,主要涉及文件打开、关闭、创建以及有关文件目录操作等的系统调用

    二级文件系统不专门设计驱动程序,要模拟文件系统的设计、实现和功能,就不能把它 直接作为操作系统实际的文件系统进行挂接。鉴于此,我在实际的硬盘上创建一个文件,把它作为我们的文件系统的磁盘进行各种对磁盘的模拟操作,这样做的好处是可以对它进行连 续操作,只要在退出文件系统时,及时保存它的状态。
    为了达到这样的效果,能方便该“磁盘文件”的操作,我们在实际的程序中调用 mmap 函数,将“磁盘文件”映射到内存中,将映射到的大内存空间当作整个二级文件系统的磁盘,直接对它进行操作。在退出或手动刷新磁盘内容时,只需调用 msync 函数将该内存空间的值 重新写入“磁盘文件”中,这样就保存了本次执行的一系列操作,在下一次再进入二级文件 系统时能够继续操作。
    2.2 设计任务剖析在本次的课程设计中,采用了简化的 UNIX V6++的设计。不同点在于:

    只考虑单用户的在线操作,去除了各种类中的锁结构,不用考虑同步或是 cpu 抢占问题等
    UNIX 世界中一切皆文件的设计,使得它的文件系统中会包含很多特殊设备的处理函数和处理判断,如字符设备。在本次课设中只存在块设备,即我们的“磁盘文件”,故删除 了关于设备号的一系列判断和使用
    在 UNIX V6++中,一个缓存块至少存在于两个队列中,每个设备有自己的设备队列。而在本次课设中只会存在一个设备——“磁盘文件”,不存在设备驱动,于是我们使用 BufferManager 类直接管理所有的缓存块,在系统中只设置一个队列。缓存块的使用逻 辑是:分配空闲缓存块时从队列头取出,Brelse 时只需将该缓存块移动到队列的尾部即 可。这也符合了最基本的 LRU 算法的思想
    磁盘文件的设计取消了磁盘最开始的 200 个引导块,SuperBlock 即为第 0 号磁盘块;在参数方面适当减小了 Diskinode 区块数和 datablock 的块数,作为实验程序够用即可

    作为二级文件系统,我们的文件系统其实与 EXT2 文件系统需要实现的功能类似,需要编写的程序要实现下列功能:

    格式化程序。在硬盘上创建一个文件,用来模拟磁盘;对磁盘按照 EXT2 文件系统结构进行划分:

    DiskInode 和一般数据块的块数定义在 FileSystem 类中,在下文的类设计中将会描述。 在格式化磁盘时主要需要初始化 SuperBlock 类和一般数据块,UNIX V6++对空闲磁盘块的管理使用成组链接法,需要在 SuperBlock 块中初始化正确的磁盘序列,将 空闲的数据块正确的分组,并为每一个分组中的第一个盘块写入索引数据。 同时格式化还需要初始化 0 号 DiskInode,它将在系统启动的时候作为指定的根目录 Inode 被直接读入内存,必须在格式化磁盘时就将其初始化成功。


    磁盘读写函数。这部分函数我取消了 UNIX V6++中复杂而可扩展的设备驱动程序,磁盘读写函数被定义在 BufferManager 类中,和 UNIX V6++相比这里只实现了延迟写函数并没有实现异步写函数,同时由于没有实现预读
    数据块分配和回收。数据块的分配和回收完整的按照成组链接法寻找空闲盘块,回 收盘块。在这里不再赘述
    索引节点分配和回收。UNIX V6++中对空闲的外存索引并没有使用成组链接法,而 是用外存索引表直接管理 100 个,当直接管理的外存 INODE 结点全部分配之后,直接搜索整个 DiskInode 区去找空闲的 i 节点,本次课程设计保持了这一设计
    目录操作函数。列表显示当前目录内目录项。其本质和读文件类似,它调用已经封 装好的fopen和fread函数去读目录文件,并打印目录文件中的存在的所有目录项
    文件操作函数。组织进程和文件系统的关系,包括用户打开文件表和系统打开文件 表的初始化和管理。创建文件、删除文件、打开文件、关闭文件、读文件和写文件。程序是引用文件描述符来操作文件的。在 UNIX V6++中系统调用的参数将被存放在 User 结构中,直接调用内核的类中函 数是不会直接传入参数的,本次课设也保留这样的设计,在我编写的 API 中会将传入的参数直接放在 User 结构中的指定变量中,在类中会对 User 结构进行操作,最 大程度的保留了 UNIX V6 的设计

    2.3 程序设计环境
    运行平台: RHEL74-X64
    编译器:posix gcc 版本 4.8.5 20150623 (Red Hat 4.8.5 - 16) (GCC)

    说明:因为是 64 系统在该版本的 gcc 编译下,指针将会是 8 位长度,这和 UNIX V6++ 的设计不同会带来一系列问题,如此时指针类型和 int 型之间的转化将会截断,C++会认为这是个不安全的操作而报错,所以需要在编译选项中加上 – m32(详细可见 Makefile 文件 )可能会需要手动安装相应的 32 位库,特此说明。
    三、详细设计3.1 类功能和详细设计3.1.1 文件系统超级块 SuperBlock定义如下:

    说明:由于是单用户模式的文件系统与 UNIX V6++相比取消了 s_flock 和 s_ilock 两个 类成员,同时需要增加两个 padding,以保证 SuperBlcok 块仍为 1024 字节,占据两个盘块。主要包含两类重要的数据:

    用于外村索引节点的管理

    s_isize:表示存储设备上的外存索引节点区占据的块数,这些值在格式化磁盘文件的时 候确定,由 FileSystem 类中的静态变量直接定义s_ninode,s_inode[100]:SuperBlock 直接管理的空闲外村索引节点数量和索引表,索引表中记录的是 s_ninode 个空闲外存索引节点的编号
    用于空闲数据块的管理

    S_fsize:表示文件系统的数据块总数S_nfree,s_free[100]:SuperBlock 直接管理的空闲块数和空闲索引表。 在操作系统初始化时,会将磁盘的 SuperBlock 读入一个内存的 SuperBlock 副本,以便于内核以更快的速度随时访问内存副本。一旦内存中的副本发生变化,会通过设置 s_fmod 标志, 由内核将内存副本写入磁盘

    3.1.2 目录结构 DirectoryEntry
    本次课设中采用和 UNIX V6++一样的树形带交叉勾连的目录结构。整个目录结构系统包含若干个目录文件,每个目录文件由一系列目录项组成。目录项是目录文件的基本构成单 位,每一个文件系统中存在的文件对一定对应某一个目录文件中的一条目录项。
    在 DirectoryEntry 中,前 4 个字节为对应文件在块设备上的外存索引节点号,它作为该 文件的内部标识,后 28 字节为文件名,是文件的外部表示,于是文件目录项为其内外部标识建立了对照关系。一块数据盘块可容纳 512/32=16 个目录项。
    值得一提的是 UNIX V6++文件系统的根目录文件,其索引节点是 DiskInode 区的 1#, 指定位置在系统初始化时装载。
    目录结构如下图:

    3.1.3 内存 Inode 节点
    Inode 类的成员部分成员变量如上,与 DiskInode 相比增加了:

    对应外存索引节点的位置信息:
    被拷贝到内存的 Inode 中的索引节点数据需要知道它来自于哪个外存 DiskInode,以便于将来内存副本被修改之后更新到外存对应的 DiskInode 中去,因此 Inode 类中包含了用于记录 DiskInode 位置信息的 i_number;
    注意在 UNIX V6++中完整的内存 Inode 还记录了设备号,而由于本次课设中只有一个 设备故删去了这一成员变量。
    Inode 状态标志位:
    I_flag 用于指示该内存 Inode 当前状态,在 UNIX V6++系统中,该标志位主要用于记录该内存 Inode 是否上锁,是否被其他进程所需要,用于多进程的同步工作,而本次课设中不需要这样的操作,所以其主要功能体现在是否需要将该内存 Inode 更新到磁盘上的“脏”置位。
    引用计数
    I_count 指出该索引节点当前活跃的实例数目。
    例如有进程通过系统调用 Open()打开文件,则该内存 Inode 用的引用计数会+1,这是系统最优化内存使用空间的做法。同时如果 用计数为 0 则表示该 Inode 空闲,可以被分配它用;
    预读判断 UNIX
    V6++中内存 Inode 会记录上次读取文件的逻辑块号以供预读判断使用,而本次课 设中没有实现预读的功能,该变量未被使用。
    成员函数的说明:

    ReadI(): 根据Inode对象中的物理磁盘块索引表,读取相应的文件数据,其中会判 断调用BufferManager中的Bread函数,是read系统调用中比较底层的封装WriteI(): 根据Inode对象中的物理磁盘块索引表,将数据写入文件。其中会调用 Bmap它会将逻辑块号转变为物理块号,通过物理块号查找内存中是否存在相应的缓存块,存在则获取,不存在则新申请。同时在写入该缓存块时要检查是否能写满一个整块,若不能则需要先读后写以保护磁盘的原始数据,注意写入缓存块后并不会立刻同步到磁盘上,标志的为延迟写Bmap(int lbn);将文件的逻辑块号转成对应的物理盘块号,这里会设计到举行文件的索引设计,不再赘述IUpdate():将内存Inode的值更新到对应的外存DiskInode中 ITrunc():释放Inode对应的文件占用的磁盘块 Clean():情况INode中的数据 ICopy(myBuf* bp, int inumber):将包含外存Inode字符块中的信息拷贝到内存 Inode中

    3.1.4 缓存控制块 Buf
    说明:Buf 控制块每一个缓存块都会对应一个缓存控制块,它会指明这个缓存块所在的 队列位置。如上文所述,由于本次课程设计中不会存在多个设备,于是我取消了所有的设备队列,缓存块只会存在与 NODEV 队列中。分配和释放的操作也非常简单,分配只是简单的 从队列头取第一个缓存块,释放时将该缓存块标志位置换后放在队列尾部。
    需要将逻辑块号初始化为-1 否则为 0 时,系统处理是不存在该块号,因为 UNIX V6++ 中不可能读 0 号盘块,而本次课设中是存在的所以需要置初值以作区分。
    3.1.5 缓存管理类 BufferManager
    如上文所述,本次课设中没有多余的设备驱动,BufferManager 将直接管理这 15 块缓存。 做初始化时,缓存块将被初始化在 NODEV 队列中,在本次课设中 NODEV 队列和自由队列是一样的。NODEV 是一个特殊的设备,表示无设备。队列头 为 bfreelist,bfreelist 同时作为自由队列的队列头。队列的形式如下图所示:

    成员函数说明:

    void Initialize(char *start):初始化 BufferManager 在本次课设中只需初始化 NODEV 队列即可(UNIX V6++这段初始化的代码写的非常优雅)myBuf* GetBlk(int blkno):申请一块缓存,用于读写 blkno,取消了 dev 设备号,该函数首先会在缓存队列中寻找是否该盘块已经在系统内存在缓存,若不存在将会从自由队列头开始搜索得到,缓存块,同时若该缓存块被置为延迟写也会将其台同 步到对应的磁盘上void Brelse(myBuf* bp):UNIX V6++中在该函数中对自由缓存队列进行初始化, 和进行释放缓存后的自由缓存队列的调整,而由于我们在分配缓存是已经调整过缓存块的位置,故在此函数中只需对缓存块的标志位进行一些处理Bread(int blkno):读一块磁盘,其中会调用 GetBlk 函数申请缓存块,UNIX V6++ 中会调用比较复杂的设备驱动等,本次课设中计算好对应的盘块位置直接用 memcpy 将其拷贝对应的缓存中Bwrite(myBuf* bp):UNIX V6++也是需要调用更底层的设备驱动来等待 IO 完成, 在本次课设中直接用 memcpy 拷贝到磁盘的指定位置Bdwrite(myBuf* bp):在延迟写的函数中只是简单的置 buf 的标志位为延迟写,在手动update 或该缓存被新分配时再调用 Bwrite 到磁盘文件中ClrBuf(myBuf* bp):情况缓存的内容Bflush():遍历所有的缓存将需要同步的缓存同步到磁盘上,在本次课设中,在系统退出的时候会执行GetBFreeList():返回缓存队列的头
    3.1.6 文件系统类 FileSystem
    说明:如上文提到的,FileSystem 中定义了格式化磁盘的各个参数,DiskInode 区的大小,数据区的长度等,这将直接影响我们的磁盘文件的大小;
    成员函数说明:

    void Initialize();初始化成员变量,简单的从Kernel类中获取全局对象 BufferManager的引用void LoadSuperBlock();系统初始化时读入SuperBlock,注意所有的读入操作都是针对缓存的,有完整的缓存查找缓存,申请缓存和Bread的步骤,UNIX V6++是正统 的面向对象的编程,各个类直接相互勾连又封装的无比完好mySuperBlock* GetFS();根据文件存储设备的设备号dev获取该文件系统的 SuperBlock,返回全局变量即内存副本Superblock的地址void Update();将SuperBlock对象的内存副本更新到存储设备的SuperBlock中去myInode* IAlloc();在存储设备dev上分配一个空闲外存INode,一般用于创建新的文件。是通过SuperBlock中的索引查到的,若直接管理的空闲Inode已经全部分配, 将会遍历整个DiskInode区去寻找空闲的Inode,于此同时重新装填直接管理的 Inode 节点;void IFree( int number); 释放存储设备dev上编号为number的外存INode,一般用于删除文件。这里只是简单将其加入SuperBlock的空闲Inode索引表中,因为在 新分配的时候会重新置位的myBuf* Alloc();在存储设备dev上分配空闲磁盘块,从 SuperBlock的索引表的栈顶 获得空闲的磁盘块编号,如果获取的编号为0,说明直接管理的索引表中没有空闲的磁盘块了,需要通过成组链接法去寻找空闲的磁盘块,对索引表做一系列的调整, 同时申请到空闲磁盘块后还需要为该磁盘块申请缓存,调用GetBlk函数,又是一番复杂的操作了,申请到之后会清空缓存中的数据,最后返回该缓存对应的Buf控制块void Free( int blkno); 释放存储设备dev上编号为blkno的磁盘块,主要是对 SuperBlock中的索引表进行操作,当索引表管理的空闲磁盘号的栈已满,会重新申 请缓存按照成组链接法的规则去做移动,并将需要释放的盘块计入索引表中即可, 在它被新分配的时候会重新清零的,如上面的函数所作的那样
    3.1.7 打开文件控制块 File 类
    说明:记录进程打开文件的读写请求类型,文件读写位置等动态信息。

    F_flag:包含对打开文件请求类型,包括读写类型,本次课设中不考虑管道类型F_inode:指向一个打开文件的 InodeF_offset:是相对应打开文件进行读写的位置指针。文建刚打开是,读写位置指针初始 值为 0,每次读写后,都将其移到已读写的下一字节F_count:是该 File 控制块的引用计数。若为 0 则表示该 File 空闲,可以分配作他用
    3.1.8 打开文件描述符表 OpenFiles 类
    说明:在本次课设中只存在一张文件打开描述符表存在于 User 结构之中,即我们只允许同时打开 15 个文件。
    该类中提供的 AllocFreeSlot()h 桉树用于在打开文件描述符表中分配一个空闲的表项。该函数线性扫描打开文件描述符表,寻找 File 指针为 NULL 的空闲项分配,并将该空闲项在OPenFIles::ProcessOpenFileTable[] 数组中的索引作为打开文件描述符 fd,返回给执行 Open() 系统调用的进程,返回的 fd 即为相应被打开文件的 OPenFIles::ProcessOpenFileTable[] 数组中的索引
    3.1.9 打开文件管理类 OpenFileTable
    说明:负责内核中对打开文件机构的管理,为进程打开文件建立内核数据结构之间的勾 连关系。 勾连关系指进程u区中打开文件描述符指向打开文件表中的File打开文件控制结构,以及从File结构指向文件对应的内存Inode。
    成员函数:

    FAlloc():在User结构中的打开文件描述符表中申请一个空闲项,即为文件句柄,然后在自己管理的系统打开文件表中寻找空闲的File结构,将申请的文件句柄与该结构勾连起 来,返回File结构的地址CloseF():关闭文件控制块的操作很简单,如果File结构中的引用次数降低到0需要释放该File结构对应的内存Inode,否则只需要将File结构中的引用次数递减即可
    3.1.10 内存 Inode 表 InodeTable
    说明:该类将会负责所有内存 Inode 的分配和释放。一组连续的内存 Inode 构成了一张 内存文件索引节点表,当打开某一文件时,如果找不到其相应的内存 Inode,就在该表中分配一个空闲项,并将该文件的外存 inode 中的主要部分拷贝进去,然后填写相应的外存 DiskInode 的地址信息。当关闭文件时,如果相应的内存 Inode 已经没有其他用处,则被放弃以便移作他用,同时在释放前如果发现其被置为已修改标志,需要将其更新到 DiskInode 上。
    成员函数:

    IGet(int inumber):该函数首先调用 IsLeaded 函数遍历内存 Inode 表查看相应的Inode 是否已经存在于内存之中,如果找到那么增加 Inode 的引用次数后直接返回该 inode,否则将会做比较复杂的申请 Inode 和读入磁盘上的 Inode 的流程IPut(myInode* pNode):减少指定的 inode 的引用计数,若计数将为 0,会判断是否仍存在目录路径指向它,以此来决定是否释放该文件占据的数据盘快,在引用计数为 0 是会直接将 inode 同步到磁盘上,即调用 IUpdateUpdateInodeTable():对内存 Inode 表中的所有 Inode 执行 update 操作,将其刷新到磁盘文件中IsLoaded(int inumber):遍历内存 inode 表,是否存在对应可用的 inodeGetFreeInode():遍历内存 inode 表,返回空闲的 inode
    3.1.11 文件管理类 FileManager
    说明:这是文件系统的各种系统调用的最高层接口,我们实现的 API 只需要调用一个 对应的成员函数就能实现相应的功能。 这个类中定义了大部分的全局对象的引用,上述的所有类都在 FileManager 中存在引用。 需要注意的是,不同的设备都有自己专属的 SuperBlock,故在 UNIX V6++中 SuperBlock 当 然不可能定义在 File Manager 中,但在本次课设中只有一个 SuperBlock,我还是在 FileManager 类中添加了对 SuperBlock 的引用。
    成员函数:

    void Initialize();初始化对全局对象的引用,同时会调用InodeTable的初始化程序 void Open();Open()系统调用处理过程,会调用NameI函数返回对应的Inode节点, 之后在以读相应的mode调用Open1函数,会经历复杂的过程打开文件,返回相应的文件句柄void Creat();Creat()系统调用处理过程,会调用NameI函数,同样返回对应文件的Inode,如果已经存在这个需要创建的文件,将会让删除这个文件,如果不存在 该文件则进入创建文件的正常流程,调用MakNode之后调用Open1返回文件句柄void Open1(myInode* pInode, int mode, int trf); Open()、Creat()系统调用的公共部分,该函数将会以不同的trf来对应不同的过程,而相同的是在该函数中需要申请空闲的File结构,建立File结构和内存Inode的勾连关系void Close();Close()系统调用处理过程,获取打开文件控制块File结构,释放打开文件描述符fd,递减File结构引用计数void Seek();Seek()系统调用处理过程,该函数会判断User结构中的参数,在按规 则修改对应的File结构中的偏移量等参数void FStat();FStat()获取文件信息void Stat();FStat()获取文件信息void Stat1(myInode* pInode, unsigned long statBuf); FStat()和Stat()系统调用的共享例程。获取文件的参数信息,本次课设中直接通过memcpy拷贝到用户提供的地址void Read();Read()系统调用处理过程,直接调用Rdwrvoid Write();Write()系统调用处理过程,直接调用Rdwrvoid Rdwr(enum myFile::FileFlags mode); 读写系统调用公共部分代码,在获得 fd对应的File结构之后,进行一系列的User结构中的参数搬运,设置偏移量和需要读写的字节数之后,分别调用WriteI和ReadImyInode* NameI(char(*func)(), enum DirectorySearchMode mode); 目录搜索, 将路径转化为相应的Inode,返回上锁后的Inodestatic char NextChar();获取路径中的下一个字符myInode* MakNode(unsigned int mode); 被Creat()系统调用使用,用于为创建新文件分配内核资源void WriteDir(myInode* pInode); 向父目录的目录文件写入一个目录项void SetCurDir(char* pathname); 设置当前工作路径int Access(myInode* pInode, unsigned int mode); 检查对文件或目录的搜索、访问权限,作为系统调用的辅助函数void ChDir();改变当前工作目录void UnLink();取消删除文件
    3.1.12 文件的 IO 参数类 IOParameter
    说明:存放文件读写时需要用到的读写偏移量,字节数以及目标区域的首地址等参数。在 User 结构中存在一个对象,其中的成员变量往往作为中间 temp,一个简单的系统调用背 后将会是极其复杂的嵌套调用,各个调用之间并不会以形参传递这些必要的参数,而是将这些参数存在 User 结构的某个对象当中,就如这个 IOParmeter 一样,在某个需要使用的函数中间再拿出来使用,而不用一直将其作形参一层层的传递。这种设计思想在我们今后自己的程序中也可以使用到。
    3.1.13 User 结构 User
    说明:相比于 UNIX V6++本次课设中的 User 结构做了大规模的简化,删除了关于进程管理的所有对象,改变了 u_ar0 的类型。只保留了与本次文件系统有关的成员。

    int u_arg[5]:这是我们的 API 存放参数的变量,在 UNIX V6++中,某一个系统调用使 用的函数往往是存放在 User 结构中的指定变量中的,例如在文件系统中的 FileManager 的 一系列系统调用接口都是无形参的,在这些函数中将会使用记录在 User 结构中的这些参数, 否则一层层传递形参,也是不小的工作量char* u_dirp:在文件系统的系统调用中经常存在 pathname 作为形参,此时作为存储参数的 Int u_arg 会比较不方便,于是在 fopen,fdelete 等 API 中 pathname 这些字符床形的参数将作为指针存储在 u_dirp 中myInode* u_cdir:指向当前目录目录文件的指针,在系统初始化时需要将其指向根目录myInode* u_pdir:指向父目录,这个变量在创建文件中十分重要,它需要需要写入目录项的目录文件其他参数在上文都有过介绍不再赘述,User 结构相当于一个 temp,它的参数在内核的所有类中都能够轻松访问到
    3.1.14 文件系统内核 Kernel 类
    说明:Kernel 用于封装本次文件系统中的全局实例对象,UNIX V6++的内核使用的是单 体模式,即在 Kernel 类其中定义一个静态的 Kernel 对象,保证内核中封装的各个内核模块的对象只有一个副本,initialize()函数即获取各个对象的引用,并调用它们的初始化函数。
    四、API 实现及主要函数分析说明:文件系统拥有超级庞大的体系,函数的嵌套调用无比复杂,在这里我只会详细叙述 fcreat API 的执行流程,考虑到详细的流程图在 word 文档上难以展示,我采用了文字、箭头、缩进的方式来尽量还原整个流程。
    4.1 系统初始化
    系统初始化时会检测是否存在 c.img 文件,如果存在则直接装载“磁盘”即可,如上文 所述,本次课程设计为了方便对“磁盘”进行操控,直接使用 mmap 函数将“磁盘”映射到 用户内存空间,直接进行操作。如果不存在 c.img 文件则会创建文件并对其进行格式化,格式化的主要内容包括:

    按照 FileSystem 的定义申请相应的数据结构初始化 SuperBlock ,填写 inod e 和空闲磁盘块的初始索引表成组链接法初始化空闲磁盘块 再将格式化的磁盘文件映射到内存空间。然后会进行二级文件系统的初始化,主要包括:
    从“磁盘”读 SuperBLock 和根目录 inode 到内存设置初始的 user 结构,主要包括当前目录等至此系统的初始化结束

    4.2 Fcreate 函数流程进入 fcreate()函数,从内核中获取 User 结构和 FileManager 的引用,将参数设置到 User结构中,调用 FileManager 中的 creat 接口 进入 FileManager.Creat()函数,获得相应类的引用后调用 NameI 函数 进入 FileManager.NameI,该函数将按路径搜索到对应项的内存 inode 节点,此时在根目录下 pInode 指向内存根目录 inode,执行 IGet 函数检查 inode 是否正在被使用,保证在目录搜索的过程中不会被释放 进入 InodeTable.IGet,调用 IsLoaded(int inumber)函数检查内存中是否有对应inode InodeTable.IsLoaded,我们在系统初始化时读入了内存 inode,很显然是能找到的找到相应内存 inode 节点,编号为 0,增加 inode 引用计数后,直接返回 inode 地址,IsLoaded()函数返回 InodeTable.IGet,找到对应 inode 返回内存 inode 地址,函数返回 FileManager.NameI,函数继续,会进行一系列的判断操作,确保搜索的是目录文件且拥有相应权限。系统第一次执行创建文件的指令,此时还没有生成根目录文件,UNIX V6++的处理方式非常的优雅,并没有立即创建根目录文件,它的处理方法是和正常创建文件一样的,此时检查到是以 creat 方式进入函数的,没有相应检索的目录项直接返回 NULL,NameI 函数返回 FileManager.Creat()函数继续执行,此时检查到 NameI 返回为 NULL,将为该文件创建自己的 i 节点和目录项,调用 MakNode 函数 进入 FIleManager.MakNode,执行 IAlloc 用来申请磁盘上空闲的 Inode 进入 FileSystem.IAlloc,获得 SuperBlock 的副本后存在直接管理的空闲 inode,直接分配,获得空闲 Inode 编号为 ino=99 再调用 IGet 将这个 inode 读入内存 进入 InodeTable.IGet,调用 IsLoaded 函数检查内存中是否存在这一副本 进入 InodeTable.IsLoaded,刚刚分配的是空闲的外存 inode 显然,内存不存在,IsLoaded 函数返回-1 InodeTable.IGet 函数继续执行,内存没有该外存 Inode,只有新申请一个空闲的内存 inode,执行 GetFreeInode 进入 GetFreeInode,遍历内存 Inode 表,直接返回空闲的 inode 地址 InodeTable.IGet 函数继续执行,获得空闲 inode 之后,调用 Bread 函数,将该外存 inode 读入缓冲区 进入 BufferManager.Bread,首先根据传入的盘块号申请缓存,调用 GetBlk 进入 BufferManager.GetBlk,首先遍历缓存队列查看队列中是否已经存在相应的缓存,在这里是一个新申请的空闲 inode 盘块,显然缓存队列中不会存在,需要重新分配,于是 GetBlk 函数会从我们的自由队列的队列头摘下首个缓存,检查它没有延迟写标志,则清空它,并将它放置到自由队列的尾部,设置初始参数,最后返回它的缓存控制块,GetBlk 函数返回 BufferManager.Bread 函数继续执行,申请到缓存块后,本次课设做的十分简单只需要找到映射到内存的“磁盘”文件的指定位置,用 memcpy 拷贝出来即可,Bread 函数成功返回 InodeTable.IGet 函数继续执行,读入整个盘块后还不够,一个盘块中存在多个 inode,我们需要提取出其中的对应 Inode 项,于是执行 ICopy 函数 进入 Inode.ICopy,该函数通过 inumber 定位到缓存中的指定地址,将它赋值给我们分配的这个内存 inode,ICopy 成功返回 InodeTable.IGet 函数继续执行,将会调用 Brelse 函数释放分配的缓存,这个函数十分简单,在这里不再列出,此后 IGet 函数成功返回 FileSystem.IAlloc 继续执行,经过复杂的操作后终于获得可使用的内存 inode清理空闲 inode 的数据,IAlloc 成功返回 inode 的指针 FIleManager.MakNode 继续执行,成功为新建的文件申请到 inode,此时将调用WriteDir 函数写入目录项了 进入 FileManager.WriteDir,将要写入的目录项 inode 号和文件名都存放在 User结构中,共下层的函数直接调用,之后调用 WriteI 函数写入父目录文件 进入 Inode.WriteI,注意这里的 WriteI 函数是由我们在 NameI 函数中提到的记录在 User 结构中的父目录 Inode 调用的,这里我们 User 结构中的偏移量只能得到逻辑地址,所以还要通过 Bmap 函数将逻辑盘块号转化为物理盘块号 进入 Inode.Bmap 函数,这里我们会查看 inode 节点中的 i_addr 文件索引结构,显然的这个初始运行的系统该目录文件根本不存在,这时候就是UNIX V6++系统的高明之处了,如之前的步骤一样虽然我们创建了这个text 文件但其实系统并没有帮他申请磁盘,在下次需要写入的时候,在Bmap函数中会帮它申请盘块。在这里对这个目录文件做的工作是一样的,对 Bmap 函数来说它不知道目录文件和普通文件的区别它通过 inode 来写入文件内容,当没有属于该 inode 的磁盘块时,它会自动帮这个文件申请空闲磁盘,于是我们系统初始化时明明不存在的根目录目录文件在此时,会被 Bmap 一步步申请空间创建出来,在这里 Bmap 会调用 Alloc 函数(注意与上文的 IAlloc 函数做区分) 进入 FileSystem.Alloc 函数,新的系统 SuperBlock 中有非常多空闲的磁盘块我们直接摘下最尾端的磁盘块,此时同样的我们需要通过高速缓存来读取和使用这个磁盘块,调用 GetBlk 函数,申请空闲缓存 进入 BufferManager.GetBlk,该函数的使用在上文有详细提及不再赘述,成功分配到可用的缓存,getBlk 将成功返回 FileSystem.Alloc 函数函数继续执行,申请到磁盘和缓存后需要清空缓存中的数据,执行 ClrBuf 函数,这个函数很简单,在这里掠过,然后 Alloc 函数成功返回 Inode.Bmap 函数继续执行,终于成功获得磁盘和缓存,在目录 inode 的所以表上填上该外存磁盘号,Bmap 返回这个物理盘块号,Bmap 成功返回 Inode.WriteI 函数继续执行,终于获得 Bmap 返回的物理磁盘号,此时判断需要写入的 32 字节不满一个盘块,于是需要先执行 Bread 函数将该磁盘的内容读出再写入缓存来避免污染原始数据,调用 Bread 函数,但其实这是有改进余地的,UNIX V6++的设计是想函数尽量的通用,我们看到 WriteI 函数简单的判断写不满一个盘块就会直接调用 Bread,但我们现在创建的其实是一个新文件,读入我们分配的空闲盘块其实是没有意义的,在我们这次课设中这个问题不显,其实在真正的操作系统中这意味着一次没有意义的磁盘IO,是很耗时的,这里还有优化的余地 进入 BufferManager.Bread 函数,这里的操作和上述调用 Bread 是类似的,他需要 GetBlk 获得缓存,再直接用 memcpy 拷贝到缓存,这里就直接略过了, Bread 函数成功返回 Inode.WriteI 函数继续执行,此时通过 User 结构中的偏移量计算出写入数据的起始位置,这是一个新文件当然是从 0 起始位置开始写,UNIX V6++的处理时将数据从 User 结构拷贝到缓存块后并不会立即更新到磁盘上,而是置该缓存延迟写的标志,然后直接释放缓存,终于执行到这里目录文件的写入算是完成了,WriteI 函数正确返回; FileManager.WriteDir 函数继续执行,处理后续工作,执行 IPut 减少根目录的引用计数,这里略过,WriteDir 函数继续返回 FIleManager.MakNode 继续执行,接收返回,MakNode 函数整个返回 FileManager.Creat()继续执行此时创建 text 文件的 inode 返回,需要执行 Open1 函数,来打开文件,这里主要处理的是文件描述符和 File 结构的分配和勾连了 进入 FileManager.Open1 函数,为该文件分配打开文件控制块 File 结构,调用FAlloc 函数(注意与上文中的 Alloc 和 IAlloc 函数做区分) 进入 OpenFileTable.FAlloc,这里调用 AllocFreeSlot 函数在进程打开文件描述符表中获得一个空闲项,这里的处理很简单,遍历数组接的返回一个空闲的fd,然后继续申请分配 File 结构同样是遍历的方法,找到空闲的 File 块后需要调用 SetF 函数建立 fd 和 File 结构的勾连关系,很简单在ProcessOpenFileTable 数组中填入即可,这里略过这两个函数的处理过程 FileManager.Open1 函数成功执行完毕,返回 FileManager.Creat()函数终于至此成功返回fcreatAPI 成功返回
    4.3 Fwrite 函数函数的主要调用关系,和书上的主要 WriteI 函数的流程图,其中写更详细的部分在 fcreat中有提到,write 的过程和写目录项是相同的。

    4.4 Fopen 函数同样的将参数传入 User 结构中后直接的调用 FileManager 类中提供的 Open 接口。同时在 Open 函数中封装的也十分简单,调用 NameI 函数后以传入的 mode 模式调用 Open1 执行完则 Open 函数返回。
    NameI 的主要功能就是通过 User 结构中的 pathname 和指向当前目录的 inode 指针通过目录文件一层层索引按文件名找到对应打开文件的 inode 并将其返回,若是找不到则返回NULL如creat()函数一样,如上文所述返NULL时creat()函数会创建inode。可以见得NameI函数也是个复用的非常好的函数,UNIX V6++中充满了这样精巧的设计,高复用的设计使得函数十分优雅,但是虽然程序员们极力使这些能够复用的函数在应对不同的情况时都能够最优而不做多余的操作,但事实上这是不可能的,就如上文中提到的系统会 Bread 为文件新分配的盘块,这就是高复用带来的一些问题——可能会有一定程度上的资源浪费。这里贴上书上的 NameI 函数的流程图,以及 UNIX V6++中的目录打开结构:


    在 NameI 返回找到的 Inode 之后将会调用 Open1 函数,和上文提到的一样,这个函数主要是申请空闲的 File 结构和空闲的文件句柄,负责这两者的分配,处理的流程比较简单,在上文也有过叙述,在这里就略过了。
    4.5 Flseek 函数在 UNIX V6++的基础上,API 的实现一般都比较简单,都是将 User 结构作为一个参数传递的 temp,然后直接调用最顶层的内核接口即可。Seek 函数的实现也是这样,直接调用FileManager 中的 Seek()函数即可。
    Seek()函数的实现比较简单,只需要通过传入的文件描述符找到自己对应的 File 结构修改其中的偏移量即可。具体实现代码如下:

    4.6 Fread 函数Fread 函数与 fwrite 的调用方式类似。并且在 FileManager 中的 Write 和 Read 函数处理方式相同,他们将以不同的调用方式调用复用 Rwdr 函数,Rwdr 函数在进行参数的处理并申请得到 File 结构块后会按照调用方式选择调用 WriteI 或者 ReadI。
    ReadI 的处理相较于 WriteI 更加简单(毕竟 WriteI 中可能也包括读文件的操作),ReadI处理的过程从宏观上来说,就是处理 User 结构确定需要读入的磁盘号,然后调用更底层的Bread 来进行磁盘和缓存的操作。下面附上书上的 ReadI 的处理流程,具体的函数嵌套关系省略。

    需要说明的是由于时间原因在本次课程设计中没有设置预读操作。
    4.7 Ls 函数Ls 函数是本次实现的 API 中唯一一个不能直接调用一个接口函数就能实现的 API 了。
    Fdelete API 对应的是 FIleManager 中的 UnLink()函数。UnLink 函数同样是调用 NameI函数寻找需要删除的文件所在目录的目录 inode,同时从 User 结构中可以获得当前需要删除文件的目录项的 inode 部分,通过 IGet 函数获得这个内存 Inode 的引用。现在我们得到了两个 inode,一个目录文件的 inode,一个需要删除文件的 inode。
    我们需要通过目录文件的 inode 找到目录文件,在原本目录项的指定位置写入清零后的目录项(这需要调用 WriteI 函数,具体的流程在上面已经叙述过)。然后利用 IPut 函数释放掉这两个 inode,事实上真正的释放操作是由 Iput 函数完成的。
    在 IPut 函数中会做判断 inode 的引用计数如果为 0,这时才会找到对应文件的数据盘快释放掉该文件的数据盘快,这个操作是由 ITrunc 函数实现的。
    ITrunc 函数需要做的是将这些盘块重新装填到 SuperBlock 的空闲盘块索引表中,当然这个操作在某些时候也是很复杂的,因为成组链接法索引的空闲盘块,SuperBlock 直接索引的空闲盘块只有 100 块,如果当前要加入的直接索引表已满,我们需要找到未满 100 块的空闲磁盘组将这一个空闲盘块加进去。虽然说来简单但只要涉及到读写的操作,我们都是通过缓存和我们的磁盘打交道的,特别是这个操作可能会涉及到多个磁盘块的读写。
    4.9 Mkdir 函数实现该 API 的实现直接调用 FIleManager 类中的 MkNod()函数。 MkNod 函数的执行流程如下:
    同样的首先调用 NameI 函数来查找获取将要创建的文件的 inode,在这里正常的 NameIm返回为mNULL,表示不存在这个目录可以创建,如果返回非 NULL,说明当前文件夹下已存在一个同名文件。很显然创建目录时不能像 Creat 的逻辑,直接删除这个文件,所以这里会直接识别为错误,函数返回。
    通过以上判断,允许创建,则会调用 MakNode 函数来创建该文件的 inode,完成所有的操作。MakNode 的函数详细执行流程在 fcreate 函数的分析中已有过叙述,在这里不再赘述。
    这样 MkNod 函数成功返回,创建目录成功。
    4.10 Cd 函数的实现Cd API 的实现对应 FileManager 中的 Chdir 函数,ChDir 的执行流程如下:

    同样首先调用 NameI 函数获取需要进入的目录的 inode 节点,很显然如果 NameI 返回NULL 没有找到该目录 inode,cd 函数会错误返回,同时 ChDir 会判断搜索到的 inode 是否对应目录文件,操作者是否有执行权限获得 inode 并经过必要的检查之后,ChDir 函数会调用 SetCurDir 函数,它会将传入的路径拷贝或加到 User 结构的当前完整目录路径 u.u_curdir 上至此 ChDir 成功返回
    4.11 backDir 函数的实现我对 backDir 实现的功能是返回上级目录,我对该 API 的处理比较简单,我会从 user结构中的u.u_curdir 中手动提取完整的上级目录,再通过 cd 函数进入那个目录。
    五、界面以及检验的流程在这里只演示下面这个非常简单的流程:

    创建一个名为 text.txt 的文件向文件中写入一定的字符打开该文件(create 函数固定是只写模式,必须重新打开)移动文件的读写指针在该位置读出一定的字符,并打印,观察是否正确查看该目录下的文件关闭该文件创建目录查看目录是否创建成功Cd 进入目录创建文件查看文件是否创建成功返回上级目录删除第一次创建的文件重新试图打开文件,查看是否会返回文件不存在
    启动程序

    这是第一次运行程序所以并没有检测到 c.img 文件,如上图所示会显示格式化磁盘完毕 当检测到存在 c.img 文件时不会存在该步操作。接下来的 Initialize 提示是初始化整个系统。
    执行创建选项

    提示 creat 成功,返回文件句柄为 0,本次课设的文件句柄是直接从 0 开始的。
    执行 fwrite 操作,写入 hello,world!十三个字符,写完毕成功返回 13.

    执行 fopen 操作,以读写权限,打开文件,返回文件句柄为 1。

    执行 fseek 操作,将文件的偏移量设置为 6

    执行 fread 操作,读出 5 个字节,可以看到读出的是 world,很显然是正确的.

    执行 ls()函数,可以看到输出的是我们刚刚创建的 test.txt 文件的文件名

    执行 mkdir 函数创建目录 testdir

    再次执行 ls 函数,可看到 dir 成功创建

    执行 cd 函数进入目录

    在这里我们新创建文件 test1

    执行 ls 函数查看该目录下的文件

    执行 backDir 函数返回上级目录

    经过 fdelete 之后再打开文件,可以看到显示打开的文件不存在:

    六、文件说明本次课设提交的文件如下


    Buf.h 中定义了 myBuf 类BufferManager.h 中定义了 myBufferManager 类File.h 中定义了 myFile,myOpenFiles,IOParemeter 三个类Filesystem.h 中定义了 mySuperBlock,myFileSystem,myDirectoryEntry 三个类Inode.h 中定义了 myInode,DiskInode 两个类Kernel.h 中主要定义了 myKernel 类Openfilemanager 中主要定义了 myOpenFIleTable,myInodeTable 两个类User.h 中主要定义了 myUser 类
    对应的 cpp 文件是对应头文件的类的实现,而 demo.cpp 中实现了磁盘的格式化,系统的初始化,和文件操作的 API。
    最后 make 出的可执行文件名为 myos
    七、总结通过本次课设更加完整深刻的理解了 UNIX V6++文件系统的设计。在阅读了 UNIX 的源码后我很难想象最初的系统是怎么由一两个人全部写完的。各种类的定义,各种函数的嵌套非常的复杂而优雅。
    在最开始动手写课设的时候比想象中的要困难很多,事实上实现整个课设的要求并不困难,在我的设想中实现这些功能大概只需要 500-600 行代码即可完成。但是如果想要移植UNIX V6++的代码则工作量大太多了。最开始我根本不知道如何入手(当然这可能是我对它的理解不够深刻),我几乎迷失在无穷无尽的类定义之中,函数一层嵌套一层。移植源码绝对比阅读理解源码难得多,我很难直接移植一个所需的函数,因为它往往要调用十几个更多的函数提供支持。最开始我苦于复杂的的设备驱动,其中的部分内联汇编更是让我不知道这东西是怎么跑起来的。
    我大概花了一个星期的课余时间来理清整个系统的函数调用关系,这时我才开始对我所需要架构的文件系统有一个比较清晰的认识。我需要一个单用户单进程的文件系统,于是UNIX 中的各种锁我不需要;我们只有一个设备,于是我不需要有设备号与十分复杂的设备缓存队列;我们的二级文件系统默认挂载,不会有新的文件挂载,于是我们不需要 UNIX 中的 mount 设计;我们的二级文件系统是通过 mmap 映射到内存的,直接调用 memcpy,于是UNIX 中复杂的设备驱动我不需要。这最终定下了我们最后二级文件系统的概貌。
    接下来是实现虽然仍然 bug 甚多,但总比开始时摸不着头脑来的顺利些。我移植所需要的代码删减掉不需要的,得到了最终的成品,编写完 API 和界面的总代码量大约在 4000 行左右,虽然大部分的代码都是移植的 UNIX V6++的代码,但完成这么大的工程确实成就感还是挺大的。
    其中深刻体会到了在较大的工程中 debug 的困难之处,我不得不承认在 linux 下我对gdbg 的使用技能几乎为 0,最终还是通过 printf 大法(执行一个函数打印很多条提示语句)的帮助完成了整个工程。
    当然本次的课设还有非常多的不足,事实上我还摘取了很多可用的内核函数,但是并没有编写更高层的 API,主要还是时间精力不太够。这学期的课设和课程巨多无比,我很难静下心来继续研究操作系统的设计。我确实觉得研究操作系统还是需要一段连续的专注的研究的。本次课设收获良多。
    1 评论 45 下载 2018-11-03 01:17:08 下载需要7点积分
  • 基于JAVA的文件系统

    一、项目需求在内存中开辟一个空间作为文件存储器,在其上实现一个简单的文件系统。退出这个文件系统时,需要该文件系统的内容保存到磁盘上,以便下次可以将其恢复到内存中来。
    二、具体技术细节
    文件存储空间管理可采取显式链接(如FAT)或者其他方法。(即自选一种方法)
    空闲空间管理可采用位图或者其他方法。如果采用了位图,可将位图和FAT表合二为一
    文件目录采用多级目录结构。至于是否采用索引节点结构,自选。目录项目中应包含:文件名、物理地址、长度等信息。同学可在这里增加一些其他信息

    三、开发工具
    环境: intellij
    语言: Java

    四、文件系统管理方案4.1 存储空间概述
    盘区数量:10
    盘区大小:1024KB

    4.2 存储空间管理方式
    FAT:在本次项目中,我的文件系统管理方案采用了显式链接的方式,在每一个盘区都开了一个文件用来存储当前盘区所有存储块的使用情况。并记录盘区内所有文件所占用内存块的情况
    优点:不存在内外内存碎片,最大程度地利用了盘区空间

    4.3 空闲空间管理方式
    位图:在本次项目中,我的文件系统空闲空间管理方案采用了位图。使用一串0和1来标识内存块的状态。0为未使用,1表示已经被占用。创建文件时从最前方开始检索空闲块,确定使用后将0置为1
    优点:一目了然,易于管理

    4.4 文件目录结构
    多级目录结构
    4.5 FCB4.5.1 概述该文件系统的FCB将直接显示在生成的txt文本中,以文本头的形势呈现。需要编辑内容可以将内容写在FCB下方的划分线内。
    4.5.2 内容
    大小
    文件名
    地址
    最近更新时间

    五、程序操作指南5.1 特殊文件说明5.1.1 BitMap&&Fat.txt
    该文件存储了盘区的存储块的状态,用0表示还未分配,1表示已经分配
    该文件存储了盘区内存在的文件按照链接方式实现的的内存分配情况,列出了对应内存块的位置

    5.1.2 recover.txt该文件是当前盘区的备份文档,在再次启动该应用程序的时候通过读取该文件的信息还原出上次的运行状态,从而实现复盘
    5.2 程序概述5.2.1 界面构成
    搜索框
    文件树
    显示框
    底部盘信息栏

    5.2.1.1 搜索框搜索框位于顶部可以在搜索栏中键入文件名或者文件夹名然后点击“start”来进行检索,如果检索成功将直接打开,失败则弹框提醒
    5.2.1.2 算法采用dfs深度搜索的方式
    // Search a file public boolean searchFile(String fileName, File parent){ File [] files = parent.listFiles(); for (File myFile:files){ if (myFile.getName().equals(fileName)){ try { if(Desktop.isDesktopSupported()) { Desktop desktop = Desktop.getDesktop(); desktop.open(myFile); return true; } } catch (IOException e1) { JOptionPane.showMessageDialog(null, myFile.getPath() + " Sorry, some thing wrong!", "Fail to open", JOptionPane.ERROR_MESSAGE); return true; } } if (myFile.isDirectory() && myFile.canRead()){ if(searchFile(fileName, myFile)){ return true; } } } return false; }
    5.2.1.3 文件树
    文件树位于左侧,呈现文件的目录结构,详情见目录结构
    5.2.1.4 显示框
    显示框位于程序右侧,用于显示文件的名称、路径、类型、大小和最近修改时间
    5.2.1.5 底部盘信息栏
    底部盘信息栏位于程序的底部,用于显示当前选中盘区的名称、已使用空间、未使用空间、文件数量
    5.2.2 菜单项
    create a file(生成一个文件)
    create a dir(生成一个文件夹)
    delete(删除一个文件或文件夹)
    format(格式化)
    rename(重命名)

    5.3 注意事项
    必须先选择一个系统内的文件夹作为模拟工作目录
    文件操作需要先选中一个文件树中的节点右键才能进行操作
    本程序重在模拟,并不是真正地为文件开了这么大的空间
    仅支持生成txt文件,在输入文件名时不需要输入.txt
    相同目录下已经存在该文件重命名将失败
    文本中将自动生成FCB,不支持用户去修改FCB
    非法输入都将导致文件生成失败
    如果存档文件recover.txt被破坏,将无法在再次打开该程序时恢复盘区信息

    六、文件系统实现功能6.1 文件目录文件目录将呈现在程序的左侧,以十个以数字命名的盘为基目录。
    点击文件夹左侧箭头可以将文件夹展开
    当执行创建和删除文件等操作,必须重新展开一次父目录才能刷新

    6.2 创建文件6.2.1 概述选中某一文件后,右键展开功能选项,选择第一项生成文件或第二项就会弹出对话框要求键入文件名和文件大小。文件生成成功之后只要重新展开父目录就可以看到新生的文件。双击程序右侧的显示区的文件就可以打开该文件看到它的FCB
    6.2.2 实现方法通过调用File类的createFile()方法来实现,文件创建成功之后将刷新FAT和位图,如果存储空间不足或是输入非法将导致文件生成失败
    // create a file public boolean createFile(String filePath){ boolean result = false; File file = new File(filePath); if(!file.exists()){ try { result = file.createNewFile(); } catch (IOException e) { e.printStackTrace(); } } return result; }
    当执行创建文件操作,必须重新展开一次父目录才能刷新

    6.3 删除文件6.3.1 概述选中某一文件后,右键展开功能选项,选择第三项删除即可删除所选项
    6.3.2 实现方法通过调用File类的delete()删除 针对文件夹需要递归删除,文件删除成功之后将刷新FAT和位图
    // delete a file public boolean deleteFile(String filePath){ boolean result = false; File file = new File(filePath); if(file.exists() && file.isFile()){ result = file.delete(); } return result; }
    当执行删除文件操作,必须重新展开一次父目录才能刷新

    6.4 重命名(更改当前目录)6.4.1 概述选中某一文件后,右键展开功能选项,选择第四项重命名即可对文件或文件夹进行重命名
    6.4.2 实现方法通过调用File类的renameTo()方法进行重命名,如果相同目录下有相同的文件,则重命名将失败
    当执行重命名文件操作,必须重新展开一次父目录才能刷新

    6.5 打开文件双击程序中右侧的显示面板中的文件即可打开对应文件
    6.6 关闭文件打开文件之后点击文件上方的关闭按钮就可以将对应文件关闭
    6.7 写文件打开文件之后在FCB的内容下方输入文本即可
    6.8 读文件双击程序右侧的显示面板中的文件即可打开对应文件进行查看
    6.9 格式化6.9.1 概述选中某一文件夹之后右键选择第四项格式化就可以将该文件夹格式化
    6.9.2 实现方法即递归删除该目录下的所有文件并更新FAT和位图
    七、 数据结构盘区(Block)
    public class Block { // 名称 private int blockName; // 盘区内的文件 private File blockFile; // 位图存储文件 private File blockBitMap; // 恢复文件 private File recover; // 位图存储文件书写器 private FileWriter bitWriter; // 恢复文件书写器 private FileWriter recoverWriter; // 文件数量 private int fileNum; // 盘区已占用空间 private double space; // 内存块数组 public int [][] bitmap = new int[32][32]; // 文件和内存块对应表 private Map<String, int[][] > filesBit = new HashMap<String, int[][]>(); // 文件列表 private ArrayList<File> files = new ArrayList<File>(); public Block(int name, File file, boolean rec) throws IOException { // 初始化 } public File getBlockFile(){ return blockFile; } public void putFCB(File file, double capacity) throws IOException { // 将FCB写入文件 } public void rewriteBitMap() throws IOException { // 重写位图存储文件 } public void rewriteRecoverWriter() throws IOException{ // 重写恢复文件 } public boolean createFile(File file, double capacity) throws IOException { // 在盘区内创建文件 } public boolean deleteFile(File file, double capacity){ // 在盘区内删除文件 } public boolean renameFile(File file, String name, double capacity) throws IOException { // 重命名盘区内的文件 } public int getFileNum() { return fileNum; } public double getSpace() { return space; }}
    1 评论 119 下载 2018-11-03 00:44:21 下载需要3点积分
  • 基于JAVA的生产者消费者问题

    一、需求分析为了更好地理解进程同步的机制和过程,决定设计实现生产者消费者问题的解决,以实现进程的同步控制。
    题目描述:有n个生产者在生产产品,这些产品将提供给m个消费者去消费,为了使生产者和消费者能并发执行,在两者之间设置一个具有k个缓冲区的缓冲池,生产者将它生产的产品放入一个缓冲区中,消费者可以从缓冲区中取走产品进行消费,显然生产者和消费者之间必须保持同步,即不允许消费者到一个空的缓冲区中取产品,也不允许生产者向一个已经放入产品的缓冲区中再次投放产品。
    由此为题,编程实现:输入生产者个数、消费者个数、缓冲区个数、每个生产者生产产品的个数,实现输出:生产者消费者同步执行情况下的具体执行过程。
    二、概要设计为了简化编码过程,将题目转变成由线程实现同步,以达到相同的目标,并采用Java实现,在控制台将程序执行的整个过程生产者消费者的执行过程输出出来。
    设计初步方案:使用Java的Thread来实现线程的生成,并继承Thread类,重写run()方法,来设定线程中的执行代码。
    生产者流程图

    消费者流程图

    三、详细设计3.1 设计思想同步机制,首先采用Java的synchronized来实现对缓冲区的互斥访问,再设置一个信号量来实现对缓冲区为空和为满的状态标记。生产者通过在synchronized同步代码块中先对缓冲区是否为满作出判断,若缓冲区为满,将当前线程添加到缓冲区的等待列表中,线程阻塞,并且在每次生产完一件产品之后唤醒缓冲区的所有等待列表;消费者通过在synchronized同步代码块中先对缓冲区是否为空作出判断,若缓冲区为空,将当前线程加入到缓冲区的等待列表中,并且在每次消费完一件产品之后唤醒缓冲区的所有等待列表。
    3.2 模块设计3.2.1 消费者线程实现类public class Consumer implements Runnable { private String name;// 线程名称 private List<Object> list;// 共享区 public Consumer(String name, List<Object> list) { this.name = name; this.list = list; } @Override public void run() { loop: while (true) { // 控制权在消费者 synchronized (list) {// 同步块,实现线程互斥 while (list.size() <= 0) // 没产品了 try { // 进程还未完成 if (!Main.completed) list.wait(); else // 进程完成,跳出外循环,结束该线程 break loop; } catch (InterruptedException e) { e.printStackTrace(); throw new RuntimeException(e); } // 缓冲区还有产品 list.remove(list.size() - 1); System.out.println("线程:" + name + " 消费了1个产品,原来有: " + (list.size() + 1) + " ,当前产品总数:" + list.size()); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } // 唤醒其他线程 list.notifyAll(); } } }}
    3.2.2 生产者线程实现类public class Producer implements Runnable { private String name;// 线程名称 private List<Object> list;// 共享区 private int productCount;// 每个生产者生产产品数 private int producerCount;// 生产者总个数 public Producer(String name, int productCount, List<Object> list, int producerCount) { this.name = name; this.list = list; this.productCount = productCount; this.producerCount = producerCount; } @Override public void run() { int i = 0; // 生产productCount个产品 while (i < productCount) { // 控制权在生产者 synchronized (list) {// 同步块,实现线程互斥 while (list.size() >= Main.bufferCount) // 缓冲区满,等待 try { list.wait(); } catch (InterruptedException e) { e.printStackTrace(); throw new RuntimeException(e); } // 缓冲区还有空间 list.add(new Object()); System.out.println("线程:" + name + " 生产了1个产品,原来有: " + (list.size() - 1) + " ,当前产品总数:" + list.size() + " ,还需生产产品数:" + (productCount - i - 1)); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } // 唤醒其他线程 list.notifyAll(); } i++; } // 生产者完成任务,刷新标记位 synchronized (Main.producerCompleted) { Main.producerCompleted.producerCompleted++;// 生产完成,记录标记 // 所有生产者均完成任务 if (Main.producerCompleted.producerCompleted == this.producerCount) { // 进程标记完成 Main.completed = true; // 通知消费者结束线程 synchronized (list) { list.notifyAll(); } } } }}
    3.2.3 进程结束信号量public class Complete { public int producerCompleted;// 生产完成的生产者数}
    3.2.4 执行代码public class Main { public static int bufferCount = 10;// 缓冲区,默认为10 public static List<Object> list = new ArrayList<Object>();// 生产好的产品列表 public static Complete producerCompleted = new Complete();// 标记生产完成了的生产者数目 public static boolean completed = false;// 标记本进程是否完成任务,完成了通知消费者结束线程 public static void main(String[] args) { producerCompleted.producerCompleted = 0;// 标记生产完成了的生产者数目,刚开始为0 Scanner scanner = new Scanner(System.in); System.out.println("请输入生产者个数:"); int producerCount = scanner.nextInt(); System.out.println("请输入消费者个数:"); int consumerCount = scanner.nextInt(); System.out.println("请输入缓冲区个数:"); bufferCount = scanner.nextInt(); System.out.println("请输入每个生产者生产产品个数:"); int productCount = scanner.nextInt(); for (int i = 1; i <= producerCount; i++) { Thread productThread = new Thread(new Producer("生产者" + i, productCount, list,producerCount)); productThread.start(); } for (int i = 1; i <= consumerCount; i++) { Thread consumerThread = new Thread(new Consumer("消费者" + i, list)); consumerThread.start(); } }}
    四、调试分析测试数据

    生产者数量:10
    消费者数量:5
    缓冲区数量:10
    每个生产者生产产品数:3

    执行结果



    结果分析
    每当缓冲区产品数到了10,也就是满,生产者就会停下来让消费者消费,每当缓冲区产品为0时,消费者就会停下来等待生产者生产产品。
    五、用户使用说明在Eclipse中点击 File——Import——Existing Projects into WorkSpace——Browser——找到ProductorConsumer文件夹的路径位置,将本工程导入到Eclipse中,然后右键工程 Run As Java Application,程序便开始执行,根据控制台文字提示输入相应的生产者数量,消费者数量,缓冲区个数,每个生产者生产产品数,便可看见程序的执行过程。
    环境说明:本工程建立在 JDK1.7 环境下。
    1 评论 6 下载 2018-11-03 00:39:21 下载需要4点积分
  • 基于JAVA的内存管理模拟

    一、需求分析为了更好地理解操作系统内存分配和管理的过程和机制,决定通过编程模拟操作系统内存分配的过程,以更好的理解操作系统内存分配过程中的具体执行流程。
    题目描述如下:
    编写一个程序,包括两个线程,一个线程用于模拟内存分配活动,另一个用于跟踪第一个线程的内存行为,要求两个线程之间通过信号量实现同步,模拟内存活动的线程可以从一个文件中读出要进行的内存操作。每个内存操作包含如下内容:

    时间:每个操作等待时间
    块数:分配内存的粒度
    操作:包括保留一个区域、提交一个区域、释放一个区域、回收一个区域、加锁与解锁一个区域。可将它们的编号放置于一个文件中

    保留是指保留进程的虚地址空间,而不分配物理地址空间提交是指在内存中分配物理地址空间回收是指释放物理地址空间,而保留进程的虚地址空间释放是指将进程的物理地址与虚拟地址空间全部释放
    大小:块的大小
    访问权限:共五种 PAGE_READONLY, PAGE_READWRIYE, PAGE_EXEXUTE, PAGE_EXEXUTE_READ, PAGE_EXEXUTE_READWRIYE

    二、概要设计根据题意,要实现两个线程,通过内存分配线程从操作文件中读取操作参数,实现模拟内存分配的过程,监听线程则监视内存分配线程的执行过程,对内存分配过程中的模拟内存详细信息进行输出。采用Java的Thread实现对线程的创建,并通过重写run()方法,来设定线程中的执行代码。
    内存分配线程流程图

    内存监视线程流程图

    三、详细设计3.1 设计思想进程的虚拟地址空间中也有三种状态的页面:空闲页面、保留页面和提交页面。空闲(Free)页面:空闲页面是指那些可以保留或提交的可用页面。保留(Reserved)页面:保留页面是逻辑页面已分配但没有分配物理存储的页面。设置这种状态的效果是可以保留一部分虚拟地址,这样,如果不预先释放这些地址,就不能被其他应用程序(如 Malloc,LocalAlloc 等)的操作所使用。试图读或写空闲页面或保留页面将导致页面出错异常。保留页面可被释放或提交。提交(Committed)页面:提交页面是物理存储(在内存中或磁盘上)已被分配的页面。可对它加以保护,不许访问或允许只读访问,或允许读写访问。提交也可以被回收以释放存储空间,从而变成保留页面。
    在本项目中,首先执行Main类中main函数的makeOperationFile()方法生成随机输入文件,其中包含对内存要求作的各种操作;然后执行Main类中main函数的memoryOperation()方法,实现输入文件所要求的各项内存管理操作。
    3.2 模块设计3.2.1项目包类图
    3.2.2 成员类说明
    Main:程序入口,执行代码,主要有生成操作文件和执行内存分配程序两个入口
    MemoryBlock:内存块类,模拟代表内存分配的内存块,一个内存块中有多个内存页,记录着内存块起始地址,内存页数,内存页区域,操作权限和锁定状态
    MemoryHandler:内存分配处理类:通过对模拟内存块和模拟内存页的操作,模拟实现内存保留、提交、锁定、解锁、回收、释放六个操作
    MemoryMonitorThread:内存监听线程,和内存分配线程同步执行,实时输出内存分配状态到文件中
    MemoryPage:模拟内存页,包含了每个页面的内存大小,默认4K
    MemoryThread:内存分配线程,和内存监视线程同步,通过读取操作文件中的操作记录实现对模拟内存的分配操作
    Mutex:内存分配线程和内存监视线程的同步信号量
    Operation:内存分配操作参数,包括内存操作大小,等待时间,操作权限,操作类型
    Trace:内存操作返回状态信息类,包括当前处理内存的虚拟地址和物理地址
    VirtualAddress:虚拟地址类,模拟实现内存的虚拟分页页号地址,包括地址其实页号,内存块页总数,内存块对应物理起始地址,操作权限,锁定状态,内存页空间

    3.2.3 内存分配线程主要执行代码: @Override public void run() { MemoryHandler memory = MemoryHandler.getInstance(); for (int i = 0; i < operations.size(); i++) { synchronized (mutex) { // 信号量不为内存操作标记,等待 while (mutex.mutex != Mutex.MUTEX_MEMORY) { try { mutex.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } Operation operation = operations.get(i); // 等待 waitForTime(operation.getTime()); // 根据不同操作类型进行相应内存操作 switch (operation.getType()) { case 0:// 保留区域 // 操作 traces[i % 5].virtualAddress = memory.reserveMemory( operation.getBlock(), operation.getPermision()); // 输出相关信息 System.out.println("保留区域"); System.out.println(operation.toString()); System.out.println("开始页号:"+ traces[i % 5].virtualAddress . .getStartPage() + ",结束页号:" + (traces[i % 5].virtualAddress.getStartPage() + traces[i % 5].virtualAddress.getBlock() - 1)); System.out.println("\n"); break; case 1:// 提交区域 if (traces[i % 5].virtualAddress != null) { // 操作 traces[i % 5].memoryBlock = memory.commitMemory(traces[i % 5].virtualAddress.getStartPage()); // 输出相关信息 System.out.println("提交区域"); System.out.println(operation.toString()); System.out.println("开始地址:" + traces[i % 5].memoryBlock.getStartAddress() + ",结束地址:" + (traces[i % 5].memoryBlock.getStartAddress() + traces[i % 5].memoryBlock.getBlock() * MemoryPage.PAGE_SIZE - 1)); System.out.println("\n"); } else System.out.println("当前没有待操作的虚拟内存地址!"); break; case 2:// 锁定区域 if (traces[i % 5].virtualAddress != null) { // 操作 memory.lockMemory(traces[i % 5].virtualAddress.getStartPage()); // 输出相关信息 System.out.println("锁定区域"); System.out.println(operation.toString()); System.out.println("开始地址:" + traces[i % 5].memoryBlock.getStartAddress() + ",结束地址:" + (traces[i % 5].memoryBlock.getStartAddress() + traces[i % 5].memoryBlock.getBlock() * MemoryPage.PAGE_SIZE - 1)); System.out.println("\n"); } else System.out.println("当前没有待操作的虚拟内存地址!"); break; case 3:// 解锁区域 if (traces[i % 5].virtualAddress != null) { // 操作 memory.unlockMemory(traces[i % 5].virtualAddress.getStartPage()); // 输出相关信息 System.out.println("解锁区域"); System.out.println(operation.toString()); System.out.println("开始地址:" + traces[i % 5].memoryBlock.getStartAddress() + ",结束地址:" + (traces[i % 5].memoryBlock.getStartAddress() + traces[i % 5].memoryBlock.getBlock() * MemoryPage.PAGE_SIZE - 1)); System.out.println("\n"); } else System.out.println("当前没有待操作的虚拟内存地址!"); break; case 4:// 回收区域 if (traces[i % 5].virtualAddress != null) { // 操作 memory.decommitMemory(traces[i % 5].virtualAddress.getStartPage()); // 输出相关信息 System.out.println("回收区域"); System.out.println(operation.toString()); System.out.println("开始地址:" + traces[i % 5].memoryBlock.getStartAddress() + ",结束地址:" + (traces[i % 5].memoryBlock.getStartAddress() + traces[i % 5].memoryBlock.getBlock() * MemoryPage.PAGE_SIZE - 1)); System.out.println("\n"); } else System.out.println("当前没有待操作的虚拟内存地址!"); break; case 5:// 释放区域 if (traces[i % 5].virtualAddress != null) { // 操作 memory.releaseMemory(traces[i % 5].virtualAddress.getStartPage()); // 输出相关信息 System.out.println("释放区域"); System.out.println(operation.toString()); System.out.println("开始页号:" + traces[i % 5].virtualAddress.getStartPage() + ",结束页号:" + (traces[i % 5].virtualAddress.getStartPage() + traces[i % 5].virtualAddress .getBlock() - 1)); System.out.println("\n"); } else System.out.println("当前没有待操作的虚拟内存地址!"); break; default: break; } // 睡眠 try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); throw new RuntimeException(e); } // 更改标记位为监听线程 mutex.mutex = Mutex.MUTEX_MONITOR; // 唤醒监听线程 mutex.notifyAll(); } } }
    3.2.4 内存监视线程主要执行代码: @Override public void run() { // 结果输出文件 File file = new File("result.txt"); if (!file.exists()) try { file.createNewFile(); } catch (IOException e1) { e1.printStackTrace(); throw new RuntimeException(e1); } FileOutputStream fos = null; try { MemoryHandler memory = MemoryHandler.getInstance(); fos = new FileOutputStream(file); // 监视内存操作,并输出内存状态 for (int i = 0; i < 30; i++) { synchronized (mutex) { // 信号量不是监听线程标记,等待内存操作线程完成 while (mutex.mutex != Mutex.MUTEX_MONITOR) { mutex.wait(); } // 向文件输出监控到的操作信息 fos.write(new String("[ 操作:" + Operation.names[i / 5] + " ]\t当前系统内存占用如下(*范围内):\r\n").getBytes()); fos.write(new String("******************************************************************\r\n").getBytes()); fos.write(memory.printStatus().getBytes()); fos.write(new String("******************************************************************\r\n\r\n\r\n").getBytes()); // 睡眠 Thread.sleep(100); // 信号量改为内存操作线程标记 mutex.mutex = Mutex.MUTEX_MEMORY; // 唤醒等待的内存操作线程 mutex.notifyAll(); } } } catch (FileNotFoundException e) { e.printStackTrace(); throw new RuntimeException(e); } catch (InterruptedException e) { e.printStackTrace(); throw new RuntimeException(e); } catch (IOException e) { e.printStackTrace(); throw new RuntimeException(e); } finally { if (fos != null) { try { fos.close(); } catch (IOException e) { e.printStackTrace(); throw new RuntimeException(e); } fos = null; } } }
    四、调试分析项目右键,执行,程序便会从项目目录下的operations.txt读取执行参数,将执行结果输出到项目目录下的result.txt中。
    由于执行结果较长,一下仅用一部分进行说明,详情请参照源码执行结果。


    五、用户使用说明在Eclipse中点击 File——Import——Existing Projects into WorkSpace——Browser——找到MemoryManager文件夹的路径位置,将本工程导入到 Eclipse 中,然后右键工程 Run As Java Application,程序便开始执行,可通过修改 Main 类中的 main 方法来实现生成操作文件和内存分配两种功能的选择。
    环境说明:本工程建立在JDK1.7环境下。
    1 评论 10 下载 2018-11-03 00:30:19 下载需要6点积分
  • 基于JAVA的进程调度算法

    一、需求分析在Java开发环境下,模拟进程调度算法,其中该算法所需要的具体功能为:采用最高优先数优先的调度算法(即把处理机分配给优先数最高的进程)和先来先服务算法(将用户作业和就绪进程按提交顺序或变为就绪状态的先后排成队列,并按照先来先服务的方式进行调度处理)。
    算法的具体描述为:每个进程有一个进程控制块( PCB)表示。进程控制块可以包含如下信息:进程名、优先数、到达时间、需要运行时间、已用CPU时间、进程状态等等。进程的优先数、需要的运行时间及到达时间可以事先人为地指定(也可以由随机数产生)。进程的运行时间以时间片为单位进行计算。每个进程的状态可以是就绪 W(Wait)、运行R(Run)、或完成F(Finish)三种状态之一。就绪进程获得 CPU后都只能运行一个时间片。用已占用CPU时间加1来表示。 如果运行一个时间片后,进程的已占用 CPU时间已达到所需要的运行时间,则撤消该进程,如果运行一个时间片后进程的已占用CPU时间还未达所需要的运行时间,也就是进程还需要继续运行,此时应将进程的优先数减1(即降低一级),然后把它插入就绪队列等待CPU。每进行一次调度程序都打印一次运行进程、就绪队列、以及各个进程的 PCB,以便进行检查。 重复以上过程,直到所要进程都完成为止。
    二、概要设计为了符合题目需求,该工程主要有三个实现类:

    Process(进程类),主要用来实例化各个不同的进程
    ProcessBlock(进程控制块类),用来为每个进程分配PCB,该类实例化后为进程类的类成员变量
    ProcessControl(进程控制类),为主类,用来调度进程。

    其中,在进程调度中,声明了三个队列,分别为待插入进程队列(按到达时间从小到大排序),就绪队列(按优先级从大到小排序,按照到达时间先后进行排序),完成队列。都ArrayList<Process>类型变量。
    调度算法描述:

    程序开始时随机为初始化5个进程(程序太多不容易观察运行结果)
    声明时间变量t,while循环下调度程序一直运行,每运行一次,t++
    然后循环判断待插入队列队首进程是否到达,若到达,则将该进程插入到就绪队列中,并从待插入队列删除该进程;若没有到达,则从该循环中跳出
    然后从就绪队列中取出队首进程并分配时间片。当该进程时间片用完后,判断该进程是否已经完成,若完成,则将该进程插入到完成队列;若没有完成,则将该进程的优先级减一并重新插入到就绪队列中
    一直重复该循环,一直到待插入队列和就绪队列都为空为止

    三、详细设计3.1 设计思想采用最高优先数优先的调度算法(即把处理机分配给优先数最高的进程)和先来先服务算法(将用户作业和就绪进程按提交顺序或变为就绪状态的先后排成队列,并按照先来先服务的方式进行调度处理)。
    3.2 模块设计所使用的数据结构(简化,仅成员变量,具体请见源码)
    public class ProcessBlock { private String name; //进程名 private int priority; //优先数 1-10 private int arriveTime; //到达时间 1-50 private int needTime; //需要时间 1-20 private int waitTime; //等待时间 1-20 private int alreadyUseTime; //已占用CPU时间 private char status; //进程状态 W, R, F}public class Process { private ProcessBlock pcb;}public class ProcessControl { private final static int uTime = 1; // 时间片 private static int time = 0; // 模拟时间 ArrayList<Process> processA = new ArrayList();// 待插入的进程 ArrayList<Process> processB = new ArrayList();// 就绪队列 ArrayList<Process> processFinish = new ArrayList();// 完成队列}

    输入子模块
    系统自动随机产生5个进程,并且随机对它们进行初始化,并按照进程产生的创建的时间进行排序插入到待插入队列中
    进程调度子模块
    采用最高优先数优先的调度算法(即把处理机分配给优先数最高的进程)和先来先服务算法(将用户作业和就绪进程按提交顺序或变为就绪状态的先后排成队列,并按照先来先服务的方式进行调度处理)对就绪队列中的进程进行进程调度
    输出子模块
    系统每调度进程一次,都会产生输出,将正在运行中的进程和就绪队列,完成队列中的进程信息全部打印出来

    四、调试分析
    遇到的问题
    当进程太多时,输出结果非常多,不利于分析系统调度算法的正确性
    解决方案
    将进程数量相应的减少之后,使输出结果相应的减少之后,使可分析程度保证在可接受的范围之内。
    调试结果(输出结果太多,仅部分输出结果)

    >各进程到达信息表:>0 name-> process4,priority-> 10,arriveTime-> 1,needTime-> 8,alreadyUseTime-> 0,waitTime-> 0,status-> W>1 name-> process1,priority-> 2,arriveTime-> 2,needTime-> 7,alreadyUseTime-> 0,waitTime-> 0,status-> W>2 name-> process2,priority-> 5,arriveTime-> 2,needTime-> 9,alreadyUseTime-> 0,waitTime-> 0,status-> W>3 name-> process0,priority-> 5,arriveTime-> 3,needTime-> 7,alreadyUseTime-> 0,waitTime-> 0,status-> W>4 name-> process3,priority-> 4,arriveTime-> 10,needTime-> 9,alreadyUseTime-> 0,waitTime-> 0,status-> W>时间:>1>正在运行的进程:>name-> process4,priority-> 10,arriveTime-> 1,needTime-> 8,alreadyUseTime-> 0,waitTime-> 0,status-> R>就绪队列>完成队列---------------------------------------------------------------->时间:>2>正在运行的进程:>name-> process4,priority-> 9,arriveTime-> 1,needTime-> 8,alreadyUseTime-> 1,waitTime-> 0,status-> R>就绪队列>name-> process2,priority-> 5,arriveTime-> 2,needTime-> 9,alreadyUseTime-> 0,waitTime-> 0,status-> W>name-> process1,priority-> 2,arriveTime-> 2,needTime-> 7,alreadyUseTime-> 0,waitTime-> 0,status-> W>完成队列---------------------------------------------------------------->时间:>3>正在运行的进程:>name-> process4,priority-> 8,arriveTime-> 1,needTime-> 8,alreadyUseTime-> 2,waitTime-> 0,status-> R>就绪队列>name-> process2,priority-> 5,arriveTime-> 2,needTime-> 9,alreadyUseTime-> 0,waitTime-> 1,status-> W>name-> process0,priority-> 5,arriveTime-> 3,needTime-> 7,alreadyUseTime-> 0,waitTime-> 0,status-> W>name-> process1,priority-> 2,arriveTime-> 2,needTime-> 7,alreadyUseTime-> 0,waitTime-> 1,status-> W>完成队列---------------------------------------------------------------->时间:>39>正在运行的进程:>name-> process1,priority-> 0,arriveTime-> 2,needTime-> 7,alreadyUseTime-> 6,waitTime-> 2,status-> R>就绪队列>name-> process3,priority-> 0,arriveTime-> 10,needTime-> 9,alreadyUseTime-> 8,waitTime-> 0,status-> W>完成队列>name-> process4,priority-> -1,arriveTime-> 1,needTime-> 8,alreadyUseTime-> 8,waitTime-> -1,status-> F>name-> process0,priority-> -1,arriveTime-> 3,needTime-> 7,alreadyUseTime-> 7,waitTime-> -1,status-> F>name-> process2,priority-> -1,arriveTime-> 2,needTime-> 9,alreadyUseTime-> 9,waitTime-> -1,status-> F---------------------------------------------------------------->时间:>40>正在运行的进程:>name-> process3,priority-> 0,arriveTime-> 10,needTime-> 9,alreadyUseTime-> 8,waitTime-> 1,status-> R>就绪队列>完成队列>name-> process4,priority-> -1,arriveTime-> 1,needTime-> 8,alreadyUseTime-> 8,waitTime-> -1,status-> F>name-> process0,priority-> -1,arriveTime-> 3,needTime-> 7,alreadyUseTime-> 7,waitTime-> -1,status-> F>name-> process2,priority-> -1,arriveTime-> 2,needTime-> 9,alreadyUseTime-> 9,waitTime-> -1,status-> F>name-> process1,priority-> -1,arriveTime-> 2,needTime-> 7,alreadyUseTime-> 7,waitTime-> -1,status-> F---------------------------------------------------------------->时间:>41>正在运行的进程:>就绪队列>完成队列>name-> process4,priority-> -1,arriveTime-> 1,needTime-> 8,alreadyUseTime-> 8,waitTime-> -1,status-> F>name-> process0,priority-> -1,arriveTime-> 3,needTime-> 7,alreadyUseTime-> 7,waitTime-> -1,status-> F>name-> process2,priority-> -1,arriveTime-> 2,needTime-> 9,alreadyUseTime-> 9,waitTime-> -1,status-> F>name-> process1,priority-> -1,arriveTime-> 2,needTime-> 7,alreadyUseTime-> 7,waitTime-> -1,status-> F>name-> process3,priority-> -1,arriveTime-> 10,needTime-> 9,alreadyUseTime-> 9,waitTime-> -1,status-> F>1.5用户使用说明>由于该程序是在java环境下编写的,故用户请在eclipse开发工具中打开项目,并运行该项目即可。
    1 评论 15 下载 2018-11-03 00:24:18 下载需要4点积分
  • 基于C++的请求分页虚拟页面替换算法

    一、需求分析实现OPT、FIFO、LRU、Clock等页面替换算法。接收用户输入参数,包括程序长度(页面数)、页框个数及页面大小,输出结果采用不同颜色区分命中、替换及直接加入空闲块。

    OPT(最佳置换算法):其所选择的被淘汰页面将是以后永不使用的,或许是在最长(未来)时间内不再被访问的页面,但是由于无法预知一个进程在内存中的若干个页面中,哪一个页面是未来最长时间内不被访问的,因而该算法无法实现
    FIFO(先进先出页面是换算法):该算法总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面予以淘汰
    LRU(最近最久未使用置换算法):只能利用“最近的过去”作为“最近的将来”的近似,因此,LRU置换算法是选择最近最久未使用的页面予以淘汰
    Clock:使用的较多的一种LRU近似算法

    二、概要设计
    FIFO算法:在该模拟系统中,使用队列来存储页面并且按照其到达队列的先后顺序进行排序,因此,每次需要进行页面替换时,便选择队首或者标识符指示其驻留时间最长的页面进行替换
    LRU算法:同样使用数组队列来模拟,数组下标指示位置,而数组值则作为标识符来计算该页面自最后一次被访问到现在为止经历了多长时间,当选择被置换的页面时,则扫描该数组队列,找到标识符最大的页面进行替换
    Clock算法:同样使用数组队列来模拟,数组下标指示内存位置,而数组值则作为标识符在替换过程中发挥作用。当选择被替换的页面时,则扫描该数组队列,检查数组值,若值为0,则进行替换;若数组值为1,则将数组值置为1,继续扫描下一个数组值。若扫描完毕没有合适的页面进行替换时,则对数组进行第二次扫描

    三、详细设计3.1 设计思想
    FIFO算法:先进先出思想,即选择在内存中驻留时间最久的页面给予淘汰
    LRU算法:根据页面调入内存后的使用情况作出决策,选择最近最久未使用耳朵页面予以淘汰
    Clock算法:设置访问位,循环检查各页面使用情况,是一种LRU近似算法

    3.2 模块设计
    输入子模块:接收用户的输入,用户的输入共有程序长度(页面数)、页框个数及页面大小三种
    模拟页面置换子模块:该模块共有三种函数,分别模拟FIFO算法,LRU算法,Clock算法

    FIFO算法描述
    void fifo(int pageNum,int pageFrameNum,int size){ int m[pageFrameNum]; for(int i = 0 ; i < pageFrameNum ; i++) m[i] = 0; //模拟页面访问 for(int i = 0 ; i < pageNum; i++) { for(int k = 0; ; k++) { for(int j = 0 ; j < pageFrameNum ; j++) { if(m[j] == k) { m[j] = k+1; for(int h = 0 ; h < pageFrameNum ; h++) { if(m[h] == 0) cout << "# "; else { if(k == 0) { if(h!=j) cout << "% "; else cout << "+ "; } else { if(h!=j) cout << "% "; else cout << "t "; } } } cout << endl; k = -1; break; } } if(k < 0 ) break; } }}
    LRU算法描述
    void lru(int pageNum,int pageFrameNum,int size){ int m[pageFrameNum]; for(int i = 0 ; i < pageFrameNum ; i++) { m[i] = 0; } for(int i = 0 ; i < pageNum; i++) { int j; for(j = 0; j < pageFrameNum; j++) { if(m[j] == 0) { m[j] = 1; for(int k = 0 ; k < pageFrameNum; k++) { if(m[k] == 0) cout << "# "; else { if(k != j) cout << "% "; else cout << "+ "; } } cout << endl; break; } } if(j > pageFrameNum-1) { int max = 0,tmp; for(int d = 0; d < pageFrameNum; d++) { if(m[d] > max) { max = m[d]; tmp = d; //该位置页面被替换 } } m[tmp] = 1; for(int f = 0; f < pageFrameNum; f++) { if(f != tmp) cout << "% "; else cout << "t "; } cout << endl; } //模拟页面访问过程 while(true) { int tmp = rand()%(pageFrameNum+1); if(m[tmp] > 0) { for(int t = 0; t < pageFrameNum; t++) { if(m[t] > 0) m[t]++; } m[tmp] = 1; } break; } }}
    Clock算法描述
    void clock(int pageNum,int pageFrameNum,int size){ int m[pageFrameNum]; for(int i = 0 ; i < pageFrameNum ; i++) { m[i] = -1; } for(int i = 0 ; i < pageNum; i++) { int j; for(j = 0; j < pageFrameNum; j++) { if(m[j] == -1) { m[j] = 1; for(int k = 0 ; k < pageFrameNum; k++) { if(m[k] == -1) cout << "# "; else { if(k != j) cout << "% "; else cout << "+ "; } } cout << endl; break; } } if(j > pageFrameNum-1) { int flag = 0,tmp; for(int d = 0; d < pageFrameNum; d++) { if(m[d] == 0) { m[d] = 1; tmp = d; flag = 1; break; } else { m[d] = 0; } } if(flag == 0) { for(int d = 0; d < pageFrameNum; d++) { if(m[d] == 0) { m[d] = 1; break; } } } for(int f = 0; f < pageFrameNum; f++) { if(f != tmp) cout << "% "; else cout << "t "; } cout << endl; } //模拟页面访问过程 while(true) { int tmp = rand()%(pageFrameNum+1); if(m[tmp] == 0) { m[tmp] = 1; } break; } }}
    输出子模块:输出页面置换后的内存调用结果,其中输出符号表示:

    t:替换
    +:直接加入空闲区
    #:页框空闲
    %:页框不空闲

    四、调试分析输入程序长度(页面数)、页框个数、页面大小分别为20,10,2,三种算法输出结果如下(其中t表示替换,+表示直接加入空闲区,#表示页框空闲,%表示页框不空闲)。
    FIFO算法

    LRU算法

    Clock算法

    五、用户使用说明该模拟系统为 C++ 语言模拟系统,请在 Codeblocks 等 C++ 编辑程序中打开编译运行,并根据提示进行输入。
    1 评论 8 下载 2018-11-03 00:07:19 下载需要6点积分
  • 基于C++的类UNIX文件系统

    一、题目要求使用一个普通的大文件(如 c:\myDisk.img ,称之为一级文件)模拟 UNIX V6++的一个文件卷,一个文件卷实际上就是一张逻辑磁盘,磁盘中存储的信息以块为单位。每块 512字节。


    磁盘文件结构

    定义自己的磁盘文件结构SuperBlock 结构磁盘 Inode 节点结构,包括:索引结构及逻辑块号到物理块号的映射磁盘 Inode 节点的分配与回收算法设计与实现文件数据区的分配与回收算法设计与实现
    文件目录结构

    目录文件结构目录检索算法的设计与实现
    文件打开结构:选作
    磁盘高速缓存:选作
    文件操作接口

    • void fformat(); 格式化文件卷 • void ls(); 列目录 • int fopen(char *name, int mode); 打开文件 • void fclose(int fd); 关闭文件 • int fread(int fd, char *buffer, int length); 读文件 • int fwrite(int fd, char *buffer, int length);写文件 • int flseek(int fd, int position); 定位文件读写指针 • int fcreat(char *name, int mode); 新建文件 • int fdelete(char *name); 删除文件

    主程序
    初始化文件卷,读入 SuperBlock图形界面或者命令行方式,等待用户输入根据用户不同的输入,返回结果

    二、需求分析根据对题目要求分析和用户使用分析,文件系统采用控制台命令作为输入,按照系统给定的命令即可完成相应的功能。
    控制台的命令应包括如下:
    " man : 手册 \n" " fformat : 格式化 \n" " exit : 正确退出 \n" " mkdir : 新建目录 \n" " cd : 改变目录 \n" " ls : 列出目录及文件 \n" " create : 新建文件 \n" " delete : 删除文件 \n" " open : 打开文件 \n" " close : 关闭文件 \n" " seek : 移动读写指针 \n" " write : 写入文件 \n" " read : 读取文件 \n" " at|autoTest : 自动测试 \n"
    每条命令具体分析如下:
    命令:fformat = "Command : fformat -进行文件系统格式化 \n" "Description : 将整个文件系统进行格式化,即清空所有文件及目录! \n" "Usage : fformat \n" "Parameter : 无 \n" "Usage Demo : fformat \n" ;
    命令:exit = "Command : exit -退出文件系统 \n" "Description : 若要退出程序,最好通过exit命令。此时正常退出会调用析构函数,\n" " : 若有在内存中未更新到磁盘上的缓存会及时更新,保证正确性。若点 \n" " : 击窗口关闭按钮,属于给当前程序发信号强制退出,不会调用析构函 \n" " : 数,未写回部分信息,再次启动时可能出现错误! \n" "Usage : exit \n" "Parameter : 无 \n" "Usage Demo : exit \n" ;
    命令:mkdir = "Command : mkdir -建立目录 \n" "Description : 新建一个目录。若出现错误,会有相应错误提示! \n" "Usage : mkdir <目录名> \n" "Parameter : <目录名> 可以是相对路径,也可以是绝对路径 \n" "Usage Demo : mkdir dirName \n" " mkdir ../dirName \n" " mkdir ../../dirName \n" " mkdir /dirName \n" " mkdir /dir1/dirName \n" "Error Avoided : 目录名过长,目录路径不存在,目录超出根目录等 \n" ;
    命令:ls = "Command : ls -列目录内容 \n" "Description : 列出当前目录中包含的文件名或目录名。若出现错误,会有相应错误提示!\n" "Usage : ls \n" "Parameter : 无 \n" "Usage Demo : ls \n" ;
    命令:cd = "Command : cd -改变当前目录 \n" "Description : 改变当前工作目录。若出现错误,会有相应错误提示! \n" "Usage : cd <目录名> \n" "Parameter : <目录名>默认为当前目录;\n" " <目录名> 可以是相对路径,也可以是绝对路径 \n" "Usage Demo : ls \n" " ls ../dirName \n" " ls ../../dirName \n" " ls /dirName \n" " ls /dir1/dirName \n" "Error Avoided : 目录名过长,目录路径不存在,目录超出根目录等 \n" ;
    命令:create = "Command : create -新建文件 \n" "Description : 新建一个文件。若出现错误,会有相应错误提示! \n" "Usage : create <文件名> <选项> \n" "Parameter : <文件名> 可以包含相对路径或绝对路径 \n" " <选项> -r 只读属性 \n" " <选项> -w 只写属性 \n" " <选项> -rw == -r -w 读写属性 \n" "Usage Demo : create newFileName -rw \n" " create ../newFileName -rw \n" " create ../../newFileName -rw \n" " create /newFileName -rw \n" " create /dir1/newFileName -rw \n" "Error Avoided : 文件名过长,目录路径不存在,目录超出根目录等 \n" ;
    命令:delet = "Command : delete -删除文件 \n" "Description : 删除一个文件。若出现错误,会有相应错误提示! \n" "Usage : delete <文件名> \n" "Parameter : <文件名> 可以包含相对路径或绝对路径 \n" "Usage Demo : delete fileName \n" " delete ../fileName \n" " delete ../../fileName \n" " delete /fileName \n" " delete /dir1/fileName \n" "Error Avoided : 文件名过长,目录路径不存在,目录超出根目录等 \n" ;
    命令:open = "Command : open -打开文件 \n" "Description : 类Unix|Linux函数open,打开一个文件。若要进行文件的读写必须先open。\n" " 若出现错误,会有相应错误提示! \n" "Usage : open <文件名> <选项> \n" "Parameter : <文件名> 可以包含相对路径或绝对路径 \n" " <选项> -r 只读属性 \n" " <选项> -w 只写属性 \n" " <选项> -rw == -r -w 读写属性 \n" "Usage Demo : open fileName -r \n" " open ../fileName -w \n" " open ../../fileName -rw \n" " open /fileName -r -w \n" " open /dir1/fileName -rw \n" "Error Avoided : 文件名过长,目录路径不存在,目录超出根目录等 \n" ;
    命令:close = "Command : close -关闭文件 \n" "Description : 类Unix|Linux函数close,关闭一个文件。可以对已经打开的文件进行关闭\n" " 若出现错误,会有相应错误提示! \n" "Usage : close <file descriptor> \n" "Parameter : <file descriptor> 文件描述符 \n" "Usage Demo : close 1 \n" "Error Avoided : 文件描述符不存在或超出范围 \n" ;
    命令:seek = "Command : seek -写入文件 \n" "Description : 类Unix|Linux函数fseek,写入一个已经打开的文件中。若出现错误,会有相应错误提示! \n" "Usage : seek <file descriptor> <offset> <origin> \n" "Parameter : <file descriptor> open返回的文件描述符 \n" " <offset> 指定从 <origin> 开始的偏移量 可正可负 \n" " <origin> 指定起始位置 可为0(SEEK_SET), 1(SEEK_CUR), 2(SEEK_END) \n" "Usage Demo : seek 1 500 0 \n" "Error Avoided : 文件描述符不存在或超出范围,未正确指定参数 \n" ;
    命令:write = "Command : write -写入文件 \n" "Description : 类Unix|Linux函数write,写入一个已经打开的文件中。若出现错误,会有相应错误提示! \n" "Usage : write <file descriptor> <InFileName> <size> \n" "Parameter : <file descriptor> open返回的文件描述符 \n" " <InFileName> 指定写入内容为文件InFileName中的内容 \n" " <size> 指定写入字节数,大小为 <size> \n" "Usage Demo : write 1 InFileName 123 \n" "Error Avoided : 文件描述符不存在或超出范围,未正确指定参数 \n" ;
    命令:read = "Command : read -读取文件 \n" "Description : 类Unix|Linux函数read,从一个已经打开的文件中读取。若出现错误,会有相应错误提示! \n" "Usage : read <file descriptor> [-o <OutFileName>] <size> \n" "Parameter : <file descriptor> open返回的文件描述符 \n" " [-o <OutFileName>] -o 指定输出方式为文件,文件名为 <OutFileName> 默认为shell \n" " <size> 指定读取字节数,大小为 <size> \n" "Usage Demo : read 1 -o OutFileName 123 \n" " read 1 123 \n" "Error Avoided : 文件描述符不存在或超出范围,未正确指定参数 \n" ;
    命令:autoTest = "Command : autoTest -自动测试 \n" "Description : 帮助测试,在系统启动初期帮助测试。测试不一定所有命令都是正确的,但是系统具有容错性,\n" " : 不会使系统异常。\n" "Usage : autoTest | at \n" "Parameter : 无 \n" "Usage Demo : at \n" ;
    三、概要设计3.1 模块划分经过分析,整个文件系统可以由以下几个部分组成:

    DeviceDriver:设备驱动模块,直接负责磁盘文件直接读写
    BufferManager:高速缓存管理模块,主要负责管理系统中所有的缓存块,包括申请、释放、读写、清空一块缓存的功能函数接口,以及系统退出时刷新所有缓存块
    FileSystem:系统盘块管理模块,主要负责对镜像文件的存储空间管理,包括SuperBlock 空间占用、DiskINode 空间分布、数据块区空间分布的管理。需要提供分配、回收 DiskINode 节点、数据块节点以及格式化磁盘文件的接口
    FileManager:系统文件操作功能实现模块,主要封装文件系统中对文件处理的操作过程,负责对文件系统访问的具体细节。包括打开文件、创建文件、关闭文件、Seek 文件指针、读取文件、写入文件、删除文件等系统功能实现
    OpenFileManager:打开文件管理模块,负责对打开文件的管理,为用户打开文件建立数据结构之间的勾连关系,为用户提供直接操作文件的文件描述符接口
    User:用户操作接口模块,主要将用户的界面执行命令转化为对相应函数的调用,同时对输出进行处理,也包含检查用户输入的正确性与合法性

    3.2 数据结构定义3.2.1 DeviceDriver:设备驱动模块
    3.2.2 BufferManager:高速缓存管理模块

    3.2.3 FileSystem:系统盘块管理模块
    3.2.4 FileManager:系统文件操作功能实现模块
    3.2.5 OpenFileManager:打开文件管理模块

    3.2.6 User:用户操作接口模块
    3.3 数据结构图示3.3.1 磁盘存储空间分布:
    3.3.2 文件索引结构:
    3.3.3 内存文件打开结构:
    3.3.4 高速缓存数据结构:
    3.4 总体流程程序的总体流程为:main 函数等待用户输入,用户输入后,对其命令进行简单解析,然后由命令查找 User 提供的操作接口,将参数交由 User 函数进行处理和判断合法性,若基本符合命令的参数约定,则由User调用FileManager中功能函数实现文件系统的具体功能。而 FileManger 中函数的具体功能则依赖于 FileSystem、BufferManager、OpenFileManager的相关函数接口调用。
    总体流程图如下:

    3.5 模块调用关系下图为模块间调用关系图示:

    四、详细设计4.1 外存磁盘文件管理外存索引节点分配 IAlloc 函数:外存 DiskINode 索引节点采用栈式管理 DiskINode,当需要分配 DiskINode 时,如果 s_ninode 不为 0,则将栈顶节点分配,栈指针减一;否则s_ninode 为 0,说明外存索引节点表中已不包含任何空闲节点,就重新搜索整个 DiskINode区,将找到的 DiskINode 号顺次登入 s_inode 表中,直到该表已满或者已搜索完整个DiskINode 区。

    外存索引节点分释放 IFree 函数:当释放 DiskINode 节点时,如果超级块索引节点表中空闲 DiskINode 数小于 SuperBlock::MAX_NINODE,则将该索引节点编号记入“栈顶”,若其中记录的空闲 DiskINode 已满,则将其散落在磁盘 DiskINode 区中。

    外存空闲盘块分配 Alloc 函数:外存中的空闲盘块采用分组链式索引法进行管理。超级块中的空闲块索引表用栈式管理空闲数据块,但是它最多只能直接管理 MAX_NFREE(100)个空闲块。所有空闲块按照每 MAX_NFREE(100)个进行构成一组,最后一组直接由超级块中的空闲索引表进行管理,其余各组的索引表分别存放在它们下一组第一个盘块的开头中。
    空闲盘块分组链式索引示例:

    分配空闲盘块时,总是从索引表中取其最后一项的值,即 s_free[—s_nfree],相当于出栈,当及时直接管理的最后一个空闲盘块时,就将该盘块的前 404 字节读入超级块的s_nfree 和索引表 s_free 数组中,使得用间接方式管理的下一组变为直接管理。

    4.2 目录搜索和地址变换目录搜索 NameI 函数:将路径名到索引节点转换的过程,与 Unix 执行的目录搜索函数基本类似,流程图如下:

    地址变换 Bmap 函数:将文件逻辑号变换成文件在存储设备的物理块号,与 Unix 执行的 Bmap 函数基本类似,流程图如下:

    4.3 内存高速缓存模拟实现Unix 系统采用自由队列和设备队列进行对缓存块的管理,但是因为本次课程设计项目是模拟 Unix 文件系统,并且整个模拟时串行执行,无并行操作,所以经过考虑,可以将自由队列和设备队列进行合并为一个缓存队列,具体细节如下:
    高速缓存初始化:即构造相应的双向循环链表,初始化成员变量。

    分离一个节点 DetashNode 函数:采用 LRU 算法,每次将第一个节点分离,第一个节点即为过去一段时间最久未使用的节点。

    插入一个节点 InsertTail 函数:采用 LRU 算法,每次将插入的节点放置在缓存队列的最尾部,视为最近使用的缓存节点。

    申请一块缓存 GetBlk 函数:申请一块缓存,将其从缓存队列中取出,用于读写设备块上的 blkno。执行过程为:首先从缓存队列中寻找是否有缓存块的 blkno 为目标 blkno,如有则分离该缓存节点,并返回该节点;没有找到说明缓存队列中没有相应节点为 blkno,需要分离第一个节点,将其从缓存队列中删除。若其带有延迟写标志,则立即写回,清空标志,将缓存 blkno 置为 blkno,返回该缓存块。

    五、调试分析5.1 测试为了方便自己程序调试和老师测试,添加了一个自动测试命令,用于测试二级文件系统容错性和正确性。
    自动测试命令较为完备,对各个环节均有考虑到,自动测试命令序列如下:
    "mkdir /dir1","mkdir dir2","create file1 -rw","create file2 -rw","ls","delete /file2","ls","mkdir /dir1/dir11","create /dir1/file11-rw","cd dir1","ls","cd ..","ls","open file1-rw","write 6 file1-2000.txt 800","seek 6 500 0","read 6 20","seek 6 500 0","read 6-o readOut1.txt 20","seek 6-20 1","read 6 100","close 6","cd dir1","ls","open file11-rw","write 6 file11-100000.txt 100000","seek 6 0 0","read 6 100","read 6 100","seek 6 50000 0","read 6 100"
    测试结果如下:



    5.2 调试中的问题解决问题 1:缓存刷新算法逻辑错误
    经过基本的功能测试后,添加了最后的 exit 命令,由前面介绍的高速缓存模拟知道,在退出时,可能有些缓存块上带有延迟写标记,尚未更新到磁盘文件中,那么在退出时需要调用虚构函数,将 Buffer 中带有延迟写标志的数据块写入到磁盘中,没有细想将缓存队列进行线性扫描写回,错误代码如下:

    调用 exit 命令后引发异常,进行了 NULL 指针的读写访问,经过仔细的逻辑思考,这个代码会造成死循环,并将循环队列尾节点的前后指针置为 NULL,接下来 pb 就变为了 NULL指针,自然就错了。不应进行按照循环队列的次序写回,不应该进行调用 Bwrite 写回。纠错后代码如下:

    问题 2:磁盘物理序号错写为数据分区盘块序号
    在做自动测试的时候,发现小文件测试没有问题,本来想欢欢喜喜以为基本完成了,但是当把文件大小设置大 100000 字节约等于 100KB 文件大小时,就出现了下面的错误:No Sapce left on device!。显然 100KB 大小是绝对可以存储的下的,于是新的问题产生了。

    通过提示来看,不难定位错误位置,通过搜索错误关键词,在 FileSystem.cpp 文件中找到了相应位置。如下红色标记:

    于是开始了问题分析:既然小文件没有问题,说明基本功能是没有问题的,文件大小为100KB 肯定会分配多个磁盘扇区,肯定是相应分配算法没有写对或者磁盘初始化的时候代码有问题,定位到磁盘初始化函数,经过仔细分析,发现将盘块在数据分区中序号写入 s_free表中,但是读取的时候是按照磁盘盘块的物理块号读取,结果就出现了以上问题。

    代码修改为:

    问题 3:分配空闲盘块后清空 Buffer 内容的 Bclear 函数错写
    解决上一个问题后,自以为这下总可以好好写报告了吧,天不遂人愿。测试没有问题,但是进行 exit 命令退出控制台时,控制台卡住几秒,然后磁盘镜像文件莫名变为了 GB 级大小。经过思考,我觉得文件这么大肯定是写入参数异常造成,而整个系统的读写都要通过BufferManager 的 Bread 和 Bwrite 函数进行读写,在函数中加入 debug 函数,打印 Buffer的相关标记信息,结果如下:

    最终的镜像文件变为了 GB 级就是因为这个 blkno 是 GB 大小的 blkno。blkno 发生错误,但是根本不知道是哪一个环节发生错误,那怎么找到错误原因呢?当然需要进一步分析何时这个编号为 45 的 Buffer 中的 blkno 变为了异常值,方法如下:

    我可以在 Bread 函数中进行条件判断,当 blkno 变为 812542311 时,设置断点,查看调用堆栈,调用关系如下:

    发现调用函数为 INode.cpp 中的 WriteI 函数,用同样的方法使函数停下来,然后查看相应局部变量。

    在此函数中进行单步执行,发现是在 Bmap()函数返回时错误,继续在 Bmap 函数中添加代码使之在指定条件停住。

    发现 Buffer 中数据与预期不和,而数据的改变只能是两个地方,一个是初始化写入时不正确,另一个是读入 Buffer 中不正确,仔细看了一下初始化的代码,并没有错误。然后看了 FileSystem.cpp 中 Alloc 分配空闲盘块时会进行清空,看了一下清空的函数,结果大吃一惊,明明要清空整个 Buffer 的内容,偏偏不知道怎么就多了一个 sizeof。

    问题 4:vs 下编译运行正确而 Linux 下出现编译及运行错误
    在 window 下 vs 集成环境下编译正确并且测试也正确之后,觉得交整个工程文件有点庞大,不够简洁,决定自己写一个 Makefile 编译,简洁舒服。但是写好 Makefile 后在 Linux下使用 G++编译就报了错误,错误如下:

    因为在 vs 下指针占用空间为四字节,int 也为四字节,没有问题。但是在 Linux 下的G++编译器中指针占用八字节空间,这样就会报精度损失错误。
    解决方法:将 arg 定义为 long 型数组,这样在 Linux 下大小为 8 字节,在 Window 下long 占用 4 字节。
    解决编译问题后,运行竟然报错了,错误为段错误。

    在 window 下编译运行正确,但是在 Linux 下发生错误,肯定是两者编译器或系统有细微区别,要想找出问题,必须在 Linux 下进行调试,选用工具为 GDB 调试。在编译命令选项中加入-g –rdynamic 可以进行 gdb 调试,调试过程如下:
    进入 GDB 调试,运行命令 r

    运行中会在相应地方报段错误的信号 SIGSEGV,找到是什么函数导致段错误。

    输入GDB命令bt打印当前函数调用栈,得知是在NameI函数中的memcpy调用出现问题。

    再次运行,设置断点为 FileManager.cpp:115 行处,准备进行单步调试查看变量。

    上面设置好断点以后,调试如下:

    发现 unsigned int nindex 这个变量不等于 string::npos 值,大概知道是因为类型转换的问题,于是写了一个测试程序如下:

    在 windows 下编译结果如下:

    同样的代码放在 Linux 下结果却不一样,原因就在这。

    由测试可以知道 Linux 64 位系统下 string::npos 的值为 unsigned long long 型,但是 windows 确是 unsigned int 型。程序错误就是 unsigned long long 截断为 unsigned int型再和 unsigned long long 型比较显然不会是 true,导致后续访问内存越界,程序段错误。更改代码如下,进行强制转换比较即可正确。

    结果验证:

    六、用户使用说明项目结构
    [1453381@sdn os]$ tree.|-- Buffer.cpp |-- Buffer.h |-- BufferManager.cpp |-- BufferManager.h |-- DeviceDriver.cpp |-- DeviceDriver.h |-- file11-100000.txt |-- file1-2000.txt |-- File.cpp |-- File.h |-- FileManager.cpp |-- FileManager.h |-- FileSystem.cpp |-- FileSystem.h|-- INode.cpp|-- INode.h|-- main.cpp|-- Makefile|-- OpenFileManager.cpp|-- OpenFileManager.h|-- readOut1.txt|-- User.cpp|-- User.h|-- Utility.cpp`-- Utility.h0 directories, 25 files
    编译方法
    可以将源程序放在集成环境下编译,也可利用 GNU 编译工具,通过写好的 Makefile 进行编译,make 如下:
    [1453381@sdn os]$ makeg++ -std=c++11 -w -c -o main.o main.cppg++ -std=c++11 -w -c -o Buffer.o Buffer.cppg++ -std=c++11 -w -c -o BufferManager.o BufferManager.cppg++ -std=c++11 -w -c -o DeviceDriver.o DeviceDriver.cppg++ -std=c++11 -w -c -o File.o File.cppg++ -std=c++11 -w -c -o FileManager.o FileManager.cppg++ -std=c++11 -w -c -o FileSystem.o FileSystem.cppg++ -std=c++11 -w -c -o INode.o INode.cppg++ -std=c++11 -w -c -o OpenFileManager.o OpenFileManager.cppg++ -std=c++11 -w -c -o User.o User.cppg++ -std=c++11 -w -c -o Utility.o Utility.cppg++ -o unix-fs main.o Buffer.o BufferManager.o DeviceDriver.o File.oFileManager.o FileSystem.o INode.o OpenFileManager.o User.o Utility.o
    运行说明
    在 Windows 下直接点击生成的 exe 文件执行,在 Linux 平台直接./unix-fs 即可运行。运行界面为控制台的命令行方式,命令较为简单,通俗易懂,初始界面如下:
    ++++++++++++++++++++ Unix 文件系统模拟 ++++++++++++++++++++Command : man -显示在线帮助手册Description : 帮助用户使用相应命令Usage : man [命令]Parameter : [命令] 如下: man : 手册fformat : 格式化exit : 正确退出mkdir : 新建目录cd : 改变目录ls : 列出目录及文件create : 新建文件delete : 删除文件open : 打开文件close : 关闭文件seek : 移动读写指针write : 写入文件read : 读取文件at|autoTest : 自动测试Usage Demo : man mkdir
    若对任何命令有不太清楚的地方,可直接 man [Command]查看详细说明,以 read 命令为例:
    [unix-fs / ]$ man readCommand : read -读取文件Description : 类 Unix|Linux 函数 read,从一个已经打开的文件中读取。若出现错误,会有相应错误提示!Usage : read <file descriptor> [-o <OutFileName>] <size>Parameter : <file descriptor> open 返回的文件描述符[-o <OutFileName>] -o 指 定 输 出 方 式 为 文 件 , 文 件 名 为<OutFileName> 默认为 shell<size> 指定读取字节数,大小为 <size>Usage Demo : read 1 -o OutFileName 123read 1 123Error Avoided : 文件描述符不存在或超出范围,未正确指定参数
    正确退出
    若要退出程序,最好通过 exit 命令。此时正常退出会调用析构函数。若有在内存中未更新到磁盘上的缓存会及时更新,保证正确性。若点 击窗口关闭按钮,属于给当前程序发信号强制退出,不会调用析构函 数,未写回部分信息,再次启动时可能出现错误!
    格式化
    格式化命令为 fformat,运行命令后系统会进行文件系统格式化,然后正常退出,再次进入时为初始环境。
    七、实验总结这学期课程设计很多,操作系统可能是自己花费时间精力最多的课程设计了。付出就有回报,通过本次操作系统课程设计,我觉得无论是系统分析和设计架构的能力,编程语言C++的运用和理解,还是寻找项目代码 bug 的能力,分析错误调试程序的能力都有较大的提升。
    要想使项目的整体规划和模块划分具有较好的合理性,做好需求分析是非常重要的。如果对系统功能要求不够明确,那么便不能很好的开展后续的工作。需求分析脚踏实地了,才能整个项目有一个较好的全局认识,然后在此基础上进行系统架构的设计和模块的划分。具体就是确定好模块的大致功能,模块之间的相互调用关系,每个功能模块类的定义,函数接口的定义,做好这些准备工作,才能一步一步稳扎稳打的实际编程。
    C++语言是建立在 C 语言之上的一门面向对象的语言,虽然从计算机入门到现在个人技术栈一直都是以 C/C++语言为主,语言的语法知识应该比较熟悉了,但是仍从本次课程设计学到不少知识。例如类的静态成员数据是不占用类的大小的,每一个类的占用空间大小是有规则定义,可能同样的成员,定义顺序不一样占用空间的大小是不一样的;不同编译器对C++语法编译规则是有细微差别的,不同操作系统运行一样的标准 C++语法代码也是有着一定区别。
    本次课程设计时间跨度比较大,本来准备先写好操作系统课程设计,写到一半又被其它课程设计和作业无情中断,导致前前后后费时较多。某些模块的小问题在这种来回切换的模式下被遗留忘记,导致后面整体测试时大费周章的寻找错误。不过这样也有好处,不知不觉中提高了自己调试代码的能力,进一步的提升了如何分析错误的原因,快速准确定位 bug所在。
    Unix 系统是现代各类 Unix 操作系统的源头,Unix v6 是非常优秀的系统程序,结构清晰,短小精悍,Unix v6++又用面向对象的思想对其重新进行了设计,并用 C++加以实现。本次课程设计就是仿照 Unix v6++中的文件系统进行改造而来,在这过程中,不免需要通透了解 Unix v6++的设计思想,详细阅读 Unix v6++中的源码,到现在可以说对其文件系统这一块的代码较为熟悉。但是 Unix v6++中的文件系统仅仅是其中的一个子部分,还有进程管理、内存管理、设备管理等模块,对整个 Unix 操作系统的进一步认识还有很长的路程要走。
    八、参考文献[1] 尤晋元,UNIX 操作系统教程[M],西安:电子科技大学出版社,1985 年 6 月
    [2] John Lions,莱昂氏 UNIX 源代码分析[M],机械工业出版社,2006 年 8 月
    [3] 汤小丹、梁红兵等,计算机操作系统[M], 西安:电子科技大学出版社,2013 年 11 月
    [4] Unix v6++源码
    1 评论 24 下载 2018-11-02 23:55:06 下载需要5点积分
显示 795 到 810 ,共 15 条
eject