分类

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

资源列表

  • 基于JSP的网络硬盘

    1 可行性分析web开发技术是Internet应用的一个重要方而,而JSP又是web开发的最先进的技术,是当前web开发人员的首选技术。
    随着网络技术的日益普及和信息化建设的重视,网络硬盘是一种新型安全的网络存储系统,已越来越受到.人们的重视和喜欢,主要适用个人文件存储,可以用作个人的一个网络U盘,网络硬盘是一块专属的存储空间,用户通过上网登录网站的方式,可以方便上传、卜载文件。只要能上网就可以用网络硬盘登录到服务器上进行个人文件的上传、删除等操作,随时随地存储自己的个人文件。
    2 需求分析2.1 系统模块设计本系统做为一个较完整的网站,主要系统的主要模块如下:

    会员注册:新会员要登录该网站享有对自己的资源进行下载或者上传资料就必须的有自己的会员号码。会员在注册的时候需要填写会员名、登录密码、确认密码等信息。如果该会员已经存在与该系统的会员信息数据库中,则提示注册失败
    会员登录:会员根据自己注册的会员号码和密码登录该系统。首次登陆会在TOMCAT目录里建一个以自己用户名命名的目录
    修改密码:会员为了自己的信息的安全可以对自己的注册时候填写的密码进行修改
    上传文件:上传自己的文件,大小格式均有限制
    下载文件:下载你自己的文件

    2.2 数据库设计本系统使用MySQL数据库建立了一个userinfo数据库,表用来存储会员的信息,还有一个admin来存储管理员账号密码。
    2.2.1 userinfo表的字段会员的注册信息存入userinfo表中,userinfo的主键是id,标准的字段说明如下:

    id 会员注册时自动增长的识别码
    userName 会员用户名
    userPassword 会员密码

    2.2.2 userinfo的详细设计如图2-1所示
    2.2.3 admin表的字段管理员的注册信息存入admin表中,admin的主键是adminName,默认值为admin,键adminPassword默认值为admin888标准的字段说明如下:

    adminName 管理员账号
    adminPassword 管理员密码

    2.2.4 admin的详细设计如图2-2所示
    3 系统管理3.1 设计说明本设计使用的JSP引擎是Tomcat6.0,使用IDE为 MyEclipse8.0。
    连接数据库使用的建立连接桥来连接数据库,因此在设计系统之前在本地数据源新建mytest的系统数据源,方便实验Java连接数据库。
    3.2 页面管理本系统所有的JSP页面都保存在KO目录中。
    用户可以通过在浏览器的地址栏中输入 http://127.0.0.1:8080/KO/index.jsp 来访问该主页。
    3.3 JavaBean本系统使用的Javabean的包名均为com.其中,BaseConn.java为链接数据库JDBC,CheckAdmin.java为检查管理员,CheckLogin.java为检查普通用户。
    3.4 后台设计后台主要为注册用户的相关操作,比如增加,删除,修改,搜索等
    4 模型图
    5 会员注册要登录该网站就必须要有会员名,因此如果没有成为会员的用户必须注册会员。在填写注册信息的使用必须填写会员名,登录密码。
    5.1 模型(Javabean)描述注册信息的Javabean中要有会员的名称、密码、年龄、性别、邮箱、电话号码以及一个注册提示信息。
    在CheckLogin.java对这些信息进行描述。具体代码如下:
    package com;import java.sql.*;public class CheckLogin { /** * 检测用户登录信息 * @param String userName * 用户登录的用户名 * @param String userPassword * 用户登录的密码 * @return String * 返回一个字符串:如果用户名已经在数据库存在并且用户输入的密码也正确 返回字符串 SUCCESS_LOGIN * 如果用户名已经在数据库存在但是输入的密码不正确 返回字符串 WRONG_PASSWROD * 如果用户名不存在返回字符串 NONE_USER * */ public String checklogin(String userName,String userPassword) throws SQLException,ClassNotFoundException { BaseConn conn = null; try { conn = new BaseConn(); //创建一个用预处理的SQL语句 String sql = "select * from userinfo where userName=?"; //创建一个预处理SQL对象 PreparedStatement ps = conn.preparedStatement(sql); ps.setString(1,userName); //从数据库中查询该用户名是否在数据库存在 ResultSet rs = conn.executeQuery(); if(rs.next()) { if(rs.getString("userPassword").equals(userPassword)) { return "SUCCESS_LOGIN"; } else return "WRONG_PASSWORD"; } else return "NONE_USER"; }catch(SQLException ex) { ex.printStackTrace(); throw ex; }catch(ClassNotFoundException ex) { ex.printStackTrace(); throw ex; } finally { conn.closeDB(); //关闭数据库连接,释放JDBC资源 } } /** * 如果是新用户时,将用户登录用户名和密码保存到数据库中 * */ public boolean saveToDataBase(String userName,String userPassword) throws SQLException,ClassNotFoundException { BaseConn conn = null; try { conn = new BaseConn(); String sql = "insert into userinfo(userName,userPassword) values(?,?)"; PreparedStatement ps = conn.preparedStatement(sql); ps.setString(1,userName); ps.setString(2,userPassword); conn.executeUpdate(); return true; }catch(SQLException ex) { ex.printStackTrace(); throw ex; }catch(ClassNotFoundException ex) { ex.printStackTrace(); throw ex; }finally { conn.closeDB(); //关闭数据库连接,释放JDBC资源 } }}
    5.2 视图(JSP页面)本模块有两个视图,register.jsp与register_post.jsp.一个提供用户填写注册信息,一个提交用户提供注册信息,把会员填写的基本信息写入userinfo表。
    注册页面的效果图如下图所示:

    该页面提交给了register_post.jsp。
    register_post.jsp代码如下所示:
    <%@ page language="java" import="java.util.*" pageEncoding="gbk"%><%@page import="com.CheckLogin"%><jsp:useBean id="check" class="com.CheckLogin"/><html> <head> <title>注册吧^_^</title> <link rel="stylesheet" type="text/css" href="styles.css"> </head><style> <body><% request.setCharacterEncoding("gbk"); //获取用户昵称 String userName = request.getParameter("userName"); //获取用户密码 String userPassword=request.getParameter("userPassword"); //将获取到的用户登录信息与数据库中保存的用户信息进行比较 String loginMsg = check.checklogin(userName,userPassword); if(loginMsg.equals("SUCCESS_LOGIN")) { out.println("该用户已经存在,请重新选择用户名"); } else if(loginMsg.equals("WRONG_PASSWORD")) { out.println("该用户已经存在,请重新选择用户名"); } else if(loginMsg.equals("NONE_USER")) { boolean sf = check.saveToDataBase(userName,userPassword); if(sf) { out.println("注册成功"); %> <br> <a href="login.jsp">返回登录</a> <% } } else { out.println("该用户名已经存在,请选择另一个用户名注册"); } %> </body></html>
    6 会员登录用户在此页面输入自己的用户名、密码登录该网站,由于本设计只设计了会员管理系统,模块之中只有个人中心是有效模块。会员进行个人中心能进行相关操作。
    6.1 模型(Javabean)此意注册为同一Javabean,CheckLogin.java
    6.2 视图(JSP视图)本模块有两个视图:login.jsp提供给用户填写登录信息,checkLogin.jsp是在验证并提交用户填写的登录信息,在主页面提供给会员该网站的全部功能。
    登录页面的效果图如下图所示:

    其中,checkLogin.jsp代码如下:
    <%@ page language="java" import="java.util.*" pageEncoding="gbk"%><jsp:useBean id="check" class="com.CheckLogin"/><% request.setCharacterEncoding("GBK"); //获取用户昵称 String userName = request.getParameter("userName"); //获取用户密码 String userPassword=request.getParameter("userPassword"); //将获取到的用户登录信息与数据库中保存的用户信息进行比较 String loginMsg = check.checklogin(userName,userPassword); if(loginMsg.equals("SUCCESS_LOGIN")) { session.setAttribute("user",userName);//建立session对象以便以后对登陆作验证 response.sendRedirect("welcome.jsp"); } else if(loginMsg.equals("WRONG_PASSWORD")) { out.println("你输入的用户名或密码错误,请检正后重新输入"); } else if(loginMsg.equals("NONE_USER")) { out.println("用户名不存在!!!"); } %><html><<head><link rel="stylesheet" type="text/css" href="styles.css"></head><body><a href=login.jsp>返回</a></body></html>
    7 主页面设为主页面为welcome.jsp,登陆后的显示如下图所示:

    welcome.jsp具体代码如下:
    <%@ page language="java" contentType="text/html; charset=gbk" pageEncoding="GB18030" import="java.sql.*,java.util.*,java.io.*"%><%@include file="conn.jsp"%><%@include file="config.jsp"%><% //当用户登入此页时,到根目录box目录新建一个以此用户名为命名的目录 String Save_Location=getServletContext().getRealPath("/")+"box//"; try{ if (!(new java.io.File(Save_Location).isDirectory())) {//如果文件夹不存在 new java.io.File(Save_Location).mkdir(); //不存在 文件夹,则建立此文件夹 new java.io.File((Save_Location)+id+"//").mkdir(); //创建文件夹,命名为当前用户的名字 } else {//存在excel文件夹,则直接建立此文件夹 new java.io.File((Save_Location)+id+"//").mkdir(); //创建文件夹,命名为当前用户的名字 } }catch(Exception e){ e.printStackTrace(); //创建文件夹失败 out.print("error"); return; } File userBox=new File((Save_Location)+id+"//"); File userBoxfile[]=userBox.listFiles();%><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"><html> <head> <title>欢迎使用网络硬盘</title> <meta http-equiv="pragma" content="no-cache"> <meta http-equiv="cache-control" content="no-cache"> <meta http-equiv="expires" content="0"> <meta http-equiv="keywords" content="keyword1,keyword2,keyword3"> <meta http-equiv="description" content="This is my page"> <link rel="stylesheet" type="text/css" href="styles.css"> </head><body> <br /> <table width="809" height="204" border="1" align="center"> <tr> <td height="26" align="left"> <font color="red">欢迎 <%=session.getAttribute("user")%>的到来!</font><font color="#0000ff">                                        </font>         <a href="update.jsp?userName=<%=session.getAttribute("user")%>">修改密码</a> <a href="logout.jsp">退出登录</a> <br> </td> </tr> <tr> <td height="58" align="left" valign="top"><a href="box_upload.jsp"><img src="images/box-upload.jpg" width="131" height="45"></a> <% for(int i=0;i<userBoxfile.length;i++){%> <tr><td height="22"> <p><font color="#FF00FF"><%=userBoxfile[i].getName()%></font></p></td><td height="25"> <span style="font-size: 9pt"> <a href="box_download.jsp?filename=<%=userBoxfile[i].getName()%>" target=_top> <font color="#0A9EE4">下载</font></a></span></td> <td height="25"><span style="font-size: 9pt"> <a href="box_del.jsp?action=confirm&filename=<%=userBoxfile[i].getName()%>" target=_top> <font color="#0A9EE4">删除</font></a></span> </td> </tr> <%} %> </td> </tr> <tr align="center"> <td height="26" align="center">  <font color="#c0c0c0"> 版本信息:重庆理工大学软件工程课程设计软件二班王兴龙</font></td> </tr> </table> </body></html>
    8 上传功能8.1 上传功能运用jspSmartUpload.jar组件,方便,灵活上传页面用box_upload_do.jsp,具体代码如下:
    <%@ page contentType="text/html; charset=gbk" language="java" import="java.util.*,com.jspsmart.upload.SmartUpload" errorPage=""%><%@ include file="conn.jsp"%><%@ include file="config.jsp"%><% // 新建一个SmartUpload对象 SmartUpload su = new SmartUpload(); // 上传初始化 su.initialize(pageContext); // 设定上传限制 // 1.限制每个上传文件的最大长度。 su.setMaxFileSize(5000000); // 2.限制总上传数据的长度。 su.setTotalMaxFileSize(150000000); // 3.设定允许上传的文件(通过扩展名限制)。 su.setAllowedFilesList("rar,zip,txt.mp3,jpg,gif,ppt,doc,xls,bmp,wav,mid,dat,ppt"); // 4.设定禁止上传的文件(通过扩展名限制),禁止上传带有exe,bat,jsp,htm,html扩展名的文件和没有扩展名的文件。 // su.setDeniedFilesList("exe,bat,jsp,htm,html,asp,php,com"); // 上传文件 su.upload(); // 将上传文件全部保存到指定目录 String cqutroot = dirPath.replace('\\', '/'); su.save(cqutroot + "box/" + id); out.print("<script>"); out.print("alert('文件上传成功!');"); out.print("location.href='welcome.jsp';"); out.print("</script>");%>
    上传页面为upload.jsp效果如下图所示:

    该页面为POST方式,提交到了box_upload_do.jsp,代码如下:
    <%@ page contentType="text/html; charset=gbk" language="java" import="java.util.*,com.jspsmart.upload.SmartUpload" errorPage=""%><%@ include file="conn.jsp"%><%@ include file="config.jsp"%><% // 新建一个SmartUpload对象 SmartUpload su = new SmartUpload(); // 上传初始化 su.initialize(pageContext); // 设定上传限制 // 1.限制每个上传文件的最大长度。 su.setMaxFileSize(5000000); // 2.限制总上传数据的长度。 su.setTotalMaxFileSize(150000000); // 3.设定允许上传的文件(通过扩展名限制)。 su.setAllowedFilesList("rar,zip"); // 4.设定禁止上传的文件(通过扩展名限制),禁止上传带有exe,bat,jsp,htm,html扩展名的文件和没有扩展名的文件。 su.setDeniedFilesList("exe,bat,jsp,htm,html,asp,php,com"); // 上传文件 su.upload(); // 将上传文件全部保存到指定目录 String cqutroot = dirPath.replace('\\', '/'); su.save(cqutroot + "box/" + id); out.print("<script>"); out.print("alert('文件上传成功!');"); out.print("location.href='welcome.jsp';"); out.print("</script>");%>
    9 下载文件9.1 jspSmartUpload.jar下载对中文支持不是很好,所以我用I/Obox_download.jsp,具体代码如下:
    <%@ page language="java" contentType="text/html; charset=gbk" import="com.jspsmart.upload.*,java.sql.*,java.io.*" %><%@ include file="config.jsp"%><%@ include file="conn.jsp"%><% java.io.BufferedInputStream bis=null; java.io.BufferedOutputStream bos=null;try{ String filename=request.getParameter("filename"); filename=new String(filename.getBytes("iso8859-1"),"gb2312"); response.setContentType("application/x-msdownload"); response.setHeader("Content-disposition","attachment; filename="+new String(filename.getBytes("gb2312"),"iso8859-1")); bis =new java.io.BufferedInputStream(new java.io.FileInputStream(config.getServletContext().getRealPath("box/"+id+"/" + filename))); bos=new java.io.BufferedOutputStream(response.getOutputStream()); byte[] buff = new byte[2048]; int bytesRead; while(-1 != (bytesRead = bis.read(buff, 0, buff.length))) { bos.write(buff,0,bytesRead); }}catch(Exception e){ e.printStackTrace();}finally { if (bis != null)bis.close(); if (bos != null)bos.close();}%>
    10 删除文件
    用户上传错误时需删除文件。
    box_del.jsp,具体代码如下
    <%@ page language="java" contentType="text/html; charset=gbk" pageEncoding="GB18030" import="java.sql.*,java.util.*,java.io.*"%><%@ include file="config.jsp"%><%@ include file="conn.jsp"%><% String cqutroot=dirPath.replace('\\','/'); //将dirPath的"\\"全替换为"/" try { //处理成中文字符 String filename=codeToString(request.getParameter("filename")); String action=codeToString(request.getParameter("action"));if(filename==null){ out.print("<script>"); out.print("alert('filename错误!');"); out.print("location.href='welcome.jsp';"); out.print("</script>");}if(action==null){ out.print("<script>"); out.print("alert('action错误!');"); out.print("location.href='box.jsp';"); out.print("</script>");}action=request.getParameter("action");if(action.equals("del")) { File delfile=new File(cqutroot+"box/"+id+"/"+filename); delfile.delete(); out.print("<script>"); out.print("alert('删除成功!');"); out.print("location.href='welcome.jsp';"); out.print("</script>"); }if(action.equals("confirm")) {%><html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><title>确认删除文件?</title><link rel="stylesheet" type="text/css" href="styles.css"></head><body><table width="646" height="345" border="1" align="center"> <tr> <td><p align="center"><font color="#ff0000"><strong>确认删除<%=filename%>?</strong></font></p></td> </tr> <tr> <td><div align="center"> <input type="button" value="提交" name="B1" onClick="javascript:location.href='box_del.jsp?action=del&filename=<%=filename%>';">    <input type="button" value="返回" name="B2" onClick="javascript:location.href='welcome.jsp';"> </div></td> </tr></table></body></html><%}else{ out.print("<script>"); out.print("location.href='welcome.jsp';"); out.print("</script>");}}catch(Exception e){ out.print("<script>"); out.print("location.href='welcome.jsp';"); out.print("</script>");}%>
    11 后台系统后台我做了这几个页面:管理员登陆:admin_login.jsp;检查管理员是否正确:admin_check.jsp;后台首页:admin.jsp;用户删除:user_del.jsp;用户密码修改:user_edit.jsp;修改保存:user_save.jsp.搜索功能:search.jsp
    12 验证是否已经真正登陆为了防止不通过验证就登陆到操作界面,做出验证页面,主要是检查session是否存在。
    其中,conn.jsp是普通用户,admin_conn.jsp是管理员的。Conn.jsp具体代码如下:
    <%@ page language="java" import="java.util.*" pageEncoding="gbk"%><% String id = (String)session.getAttribute("user"); if (null==id || id.equals("")) { out.print("<script language='javascript'>alert('请先登录系统!');window.location = 'login.jsp';</script>"); }%>
    admin_conn.jsp的代码如下所示:
    <%@ page language="java" import="java.util.*" pageEncoding="gbk"%><% String admin_id = (String)session.getAttribute("admin"); if (null==admin_id || admin_id.equals("")) { out.print("<script language='javascript'>alert('请先登录系统!');window.location = 'admin_login.jsp';</script>"); }%>
    13 辅助页面获取程序文件夹的绝对路径功能和中文字符串的转换是好多页面都共有的,存在一个页面调用起来比较方便,Config.jsp代码如下:
    <%@ page language="java" contentType="text/html; charset=gbk"%><%//获取程序文件夹的绝对路径String dirPath=application.getRealPath(request.getRequestURI());dirPath=dirPath.substring(0,dirPath.lastIndexOf('\\')+1);dirPath=dirPath.substring(0,dirPath.lastIndexOf('\\'));dirPath=dirPath.substring(0,dirPath.lastIndexOf('\\')+1);%><%!//定义处理中文字符串的函数public String codeToString(String str01) throws Exception{//处理中文字符串的函数 String s=str01; if(true) { try { byte tempB[]=s.getBytes("ISO-8859-1"); s=new String(tempB); } catch(Exception e) { s=str01; } } return s;}%>
    14 退出系统会员在网站中可以选择退出系统,logon.jsp负责销毁用户的session对象,导致登录失效,用户能够随时退出系统。
    logon.jsp的代码如下:
    <%@ page language="java" import="java.util.*" pageEncoding="ISO-8859-1"%><% session.invalidate(); response.sendRedirect("login.jsp");%>
    15 设计总结通过本次设计,基本掌握了WEB设计的页面设计,首次接触学习并使用JSP,JAVABEAN以及文件的上传下载,数据的连接等知识,对以后的深入学习打下了基础。就这个设计而言,由于时间以及个人能力的限制,在网站编写的过程中有很多的不足。许多老师要求的功能都没有达到,比如文件的搜索与共享等,只是能初步实现功能。在这方面应该加强学习,争取取得好的成绩。最后谢谢老师给我们这次的课程设计锻炼的机会。
    参考文献[1] 王柯柯,Web网页设计技术,机械工业出版社,2011
    [2] 马建红,李占波,JSP应用与开发技术,2011
    [3] 常建功,Java Web典型模块与项目实战大全,2010
    [4] 史晓红,章立, Dreamweaver CS4网页设计与实训教程,2010
    1 评论 20 下载 2018-11-04 15:31:18 下载需要7点积分
  • 基于JAVA的记事本

    一、绪论1.1 引言现如今,电脑已经成为了每家每户甚至是每个人手头都必有的一种实用性工具,它改变了人们的生活,大大提高了人们的工作效率。在此基础上,电脑端的记事本应用一直是每台电脑所必备的实用性应用,不管是在台式电脑、笔记本电脑或者平板电脑上,都能看到它的身影。其功能基本有如下几种:文件、编辑、格式、查看、帮助,每个功能下又有多个子功能,为使用者提供了多种编辑上的便利,基本能满足人们记事的需求,特别是快速笔记。正因为它的这些特点,才让它成为每台电脑中必不可少的成分。
    1.2 编写目的电脑端记事本是每台电脑的标配,有相当大的实用性,方便人们平时的记事之用,尤其是在快速笔记这方面,更是有非常大的作用,基本能满足人们的记事需求,有很大的开发及继续完善开发的意义。
    基于记事本的诸多优点,本课程设计针对电脑端的记事本进行开发设计,并在原有基础上进行完善,使它的功能更完善、更人性化及更实用化。
    1.3 背景随着人们生活信息化的提高,记事本只拘泥于笔和纸的时代已经一去不复返了,越来越多的电子版记事本进入了人们的生活。但如今的电脑端记事本软件感觉功能不够丰富,缺少一些个性化功能,导致用户体验不是很好,故本课程设计将开发一个加强版的电脑端记事本,来满足用户的需求。
    二、系统可行性研究2.1 系统概述2.1.1 当前系统分析当前电脑系统自带的记事本实现的功能有如下几种:文件、编辑、格式、查看、帮助,每个功能下又有多个子功能:

    “文件”主菜单中有“新建”、“打开”、“保存”、“另存为”、“页面设置”、“打印”、“退出”这几个子功能
    “编辑”主菜单中有“撤销”、“剪切”、“复制”、“粘贴”、“删除”、“查找”、“查找下一个”、“替换”、“转到”、“全选”、“日期/时间”这几个子功能
    “格式”主菜单中有“自动换行”、“字体”这两个子功能
    “查看”主菜单中有“状态栏”子功能
    “帮助”主菜单中有“查看帮助”、“关于记事本”这两个子功能

    2.1.2 目标系统分析在实现系统自带笔记本的功能同时,再添加一些个性化功能,例如为记事本添加上行号(这大大提高了我们程序员看代码的方便性),在状态栏添加上当前时间以及字数统计,让用户能够对自己所写的字数一目了然,大大增强了用户体验。
    此外,此记事本支持用户自定义背景颜色以及字体颜色,增强了趣味性,用户可以根据自己的喜好选择符合自己的主题。
    即实现的功能有:

    “文件”主菜单中有“新建”、“打开”、“保存”、“另存为”、“页面设置”、“打印”、“退出”这几个子功能
    “编辑”主菜单中有“撤销”、“剪切”、“复制”、“粘贴”、“删除”、“查找”、“查找下一个”、“替换”、“转到”、“全选”、“日期/时间”这几个子功能
    “格式”主菜单中有“自动换行”、“字体”、“背景颜色”、“字体颜色”这四个子功能
    “查看”主菜单中有“状态栏”子功能
    “帮助”主菜单中有“查看帮助”、“关于记事本”这两个子功能

    2.2 可行性分析研究2.2.1 技术可行性由于计算机技术和互联网技术的发展突飞猛进,计算机的应用深入各行各业。涌现出各种编程语言。本电脑端记事本采用JAVA语言进行开发设计。JAVA语言是一门面向对象的语言,风格接近C、C++语言,但又舍弃了C和C++语言中易引起错误的指针、运算符重载、多重继承等特性,使开发的程序质量更高。由于开发记事本的难度不高,因此通过JAVA语言在Eclipse编译器上就可以实现开发了。
    综上,技术可行性满足。
    2.2.1.1 Java的基本信息及优势Java是一种可以撰写跨平台应用程序的面向对象的程序设计语言。Java 技术具有卓越的通用性、高效性、平台移植性和安全性,广泛应用于PC、数据中心、游戏控制台、科学超级计算机、移动电话和互联网,同时拥有全球最大的开发者专业社群。
    与传统程序不同,Sun 公司在推出 Java 之际就将其作为一种开放的技术。全球数以万计的 Java 开发公司被要求所设计的 Java软件必须相互兼容。“Java 语言靠群体的力量而非公司的力量”是Sun公司的口号之一,并获得了广大软件开发商的认同。这与微软公司所倡导的注重精英和封闭式的模式完全不同。
    Sun 公司对 Java 编程语言的解释是:Java 编程语言是个简单、面向对象、分布式、解释性、健壮、安全与系统无关、可移植、高性能、多线程和静态的语言。
    Java 平台是基于 Java 语言的平台。这样的平台非常流行。因此微软公司推出了与之竞争的.NET平台以及模仿Java的C#语言。
    Java是功能完善的通用程序设计语言,可以用来开发可靠的、要求严格的应用程序。
    2.2.1.2 Eclipse的基本信息Eclipse 是一个开放源代码的、基于Java的可扩展开发平台。就其本身而言,它只是一个框架和一组服务,用于通过插件组件构建开发环境。幸运的是,Eclipse 附带了一个标准的插件集,包括Java开发工具(Java Development Kit,JDK)。

    2.2.2 经济可行性主要分为三方面进行分析,分别是开发的财力物力及时间。

    开发的财力物力:

    笔记本电脑X1其他成本几乎为零,因为该项目开发的难度不大,完成时即刻可以使用,也不需要另外研发硬件设施进行使用,用电脑就行
    开发的时间:

    从一开始的分析设计到最后的测试维护,时间约为一周就可以,时间成本不大,可行性高
    收益:

    由于开发这个程序可以更好地满足人们的日常需求,收益还算不错的

    综上,经济可行性满足。
    2.2.3 操作可行性本程序采用的是图形化界面方式,记事本的操作不难,一般会使用电脑的人都会操作,只需按照图形界面进行操作,而且每个操作都有相关的快捷键提示,不需要相关的操作指导即可使用,可操作性非常强。
    2.2.4 社会可行性根据前期电脑上的记事本的使用情况及普及率来看,记事本的功能是受社会所认可的,人们普遍接受及使用电脑上的记事本,是可以为社会带来利益的。因此,对电脑端的记事本进行再开发完善,发掘它更多的功能并创造出社会价值,可行性是很高的。
    三、需求分析3.1 系统功能概述
    3.2 系统功能描述3.2.1 功能图
    3.2.2 用例描述


    用例名称
    新建




    涉及的参与者
    用户


    用例描述
    在Windows 环境下,新建一个空白txt文档


    前置条件
    记事本系统可用


    正常事件流
    1.点击开始-所有程序-附件-记事本 2.在记事本系统界面,点击文件-新建






    用例名称
    打开




    涉及的参与者
    用户


    用例描述
    在Windows 环境下,新建一个空白txt文档


    前置条件
    记事本系统可用


    正常事件流
    1. 双击打开记事本 2. 左键单击记事本-点击打开






    用例名称
    保存或另存为




    涉及的参与者
    用户


    用例描述
    在Windows 环境下,新建一个空白txt文档


    前置条件
    记事本系统可用


    正常事件流
    1. 打开空白记事本,点击文件-保存-再次打开 2. 点击文件-另存为-保存






    用例名称
    编辑




    涉及的参与者
    用户


    用例描述
    在Windows 环境下,新建一个空白txt文档


    前置条件
    记事本系统可用


    正常事件流
    1. 在编辑区域输入“软件工程课程设计” 2. 在编辑区域输入“1504” 3. 选中内容“软件工程课程设计”,点击编辑-剪切 4. 点击编辑-撤销 5. 选中内容“软件工程课程设计”,点击编辑-复制 6. 点击编辑-粘贴 7. 选中内容“软件工程课程设计”,点击编辑-删除 8.点击编辑-查找,查找内容“软件工程课程设计” 9. 点击编辑-查找下一个






    用例名称
    字体




    涉及的参与者
    用户


    用例描述
    在Windows 环境下,新建一个空白txt文档


    前置条件
    记事本系统可用


    正常事件流
    1. 在记事本系统界面,点击格式-字体 2. 在编辑区域输入“软件工程课程设计”,字体改为Wingdings,点击确定 3. 选择字体-斜体 4. 选择字体-8



    3.3 系统功能实现3.3.1 时序图
    3.3.2 类图(只选取部分函数)
    3.4 系统角色用户可以在系统里创建新文件,编辑文字,并保存,以便查看,还可以进行批处理文件等操作。
    3.5 系统流程图3.5.1 主流程图如下图所示,打开记事本后,可在文本域进行文本输入;或者进行一系列执行操作包括:文件、编辑、格式以及帮助菜单。

    功能描述:打开文件、编辑、格式以及帮助菜单
    输入项:点相关按钮
    输出项:做出指定动作


    3.5.2文件菜单操作流程图文件菜单操作流程如下图,当打开文件菜单时,下拉显示子菜单包括新建、打开、保存、退出等功能。

    功能描述:实现文件新建、打开、保存、另存为、退出功能
    输入项:点相关按钮
    输出项:做出指定动作


    3.5.3 编辑菜单操作流程图编辑菜单操作流程如下图,当打开编辑菜单时,下拉显示子菜单包括复制、粘贴、剪切、全选、查找、替换等功能。

    功能描述:实现文本的剪切、复制、粘贴、撤销、查找、替换、删除功能
    输入项:点相关按钮
    输出项:做出指定动作


    3.5.5帮助菜单操作流程图帮助菜单操作流程如下图,当打开帮助菜单时,下拉显示子菜单关于记事本。

    功能描述:查看帮助信息
    输入项:点帮助按钮
    输出项:显示相关信息


    3.6 数据流图
    3.7 数据字典由于无数据库设计,所以无数据字典描述。
    四、概要设计4.1 系统运行环境4.1.1 操作系统
    操作系统无关性,Windows XP/7/8.1/10、Linux、Mac OS X下安装了Java的运行环境JRE即可运行
    JDK版本:1.8

    4.1.2 使用软件
    代码编写:Eclipse
    数据库:无
    建模工具:Rational Rose(自写)
    文档编写:Microsoft Word 2016

    4.1.3 开发语言
    Java
    4.2 系统总体设计4.2.1 基本设计概念和处理流程4.2.1.1 全局E-R图
    4.2.1.2 分E-R图


    4.2.2 系统总体结构与模块

    4.3 接口设计4.3.1 外部接口4.3.1.1 用户界面
    4.3.1.2 软件接口软件运行平台:Eclipse
    4.3.1.3 硬件接口硬件运行平台:PC端
    4.3.2 内部接口说明各个模块由谁调用,完成什么功能,完成后转入什么状态



    模块
    由谁调用
    功能
    完成后转入的状态




    clock
    NotepadMainFrame
    显示状态栏的时间



    MQFontChooser
    NotepadMainFrame
    字体选择器
    字体变更


    textLine
    NotepadMainFrame
    左边的行号



    状态栏
    NotepadMainFrame
    状态栏显示信息
    显示状态栏


    MainFrameWidowListener
    NotepadMainFrame
    退出窗口选择设置
    退出软件


    actionPerformed
    NotepadMainFrame
    记事本的状态操作
    更换操作模式


    exit,mySearch,paste等
    NotepadMainFrame
    退出,查找,暂停等操作
    记事本状态改变



    4.4 数据结构设计由于没有用到数据库设计,所以无数据结构设计。
    五、详细设计5.1 数据流图
    源点/终点:用户
    处理:编辑(包括撤销、复制、剪贴、粘贴等)、新建记事本、保存、打开记事本、格式、打印
    数据流:输入的字符,记事本,打印文档
    数据存储:本地磁盘

    此记事本的基本系统模型如下图

    细化后的数据流图如下图

    5.2 层次方框图
    5.3 功能模块图
    六、系统实现6.1 关键代码6.1.1 自动换行JTextArea有自己定义的策略。
    //设置文本区的换行策略。如果设置为 true,则当行的长度大于所分配的宽度时,将换行。此属性默认为 falsetextArea.setLineWrap(true);
    6.1.2背景颜色JColorChooser jcc1 = new JColorChooser();JOptionPane.showMessageDialog(this, jcc1,"选择背景颜色颜色",-1);color = jcc1.getColor();textArea.setBackground(color);

    6.1.3 字体颜色jcc1=new JColorChooser();JOptionPane.showMessageDialog(this, jcc1, "选择字体颜色", -1);color = jcc1.getColor();//String string=textArea.getSelectedText();textArea.setForeground(color);
    6.1.4鼠标右击菜单// 创建弹出菜单final JPopupMenu jp=new JPopupMenu(); //创建弹出式菜单,下面三项是菜单项textArea.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if(e.getButton()==MouseEvent.BUTTON3)//只响应鼠标右键单击事件 { jp.show(e.getComponent(),e.getX(),e.getY());//在鼠标位置显示弹出式菜单 } }});

    6.1.5 打印功能 public void Print() { try{ p = getToolkit().getPrintJob(this,"ok",null);//创建一个Printfjob 对象 p g = p.getGraphics();//p 获取一个用于打印的 Graphics 的对象 //g.translate(120,200);//改变组建的位置 this.textArea.printAll(g); p.end();//释放对象 g } catch(Exception a){ } }

    6.1.6 显示时间//用到了线程,每1秒刷新一次时间public class Clock extends Thread{ public void run() { while (true) { GregorianCalendar time = new GregorianCalendar(); int hour = time.get(Calendar.HOUR_OF_DAY); int min = time.get(Calendar.MINUTE); int second = time.get(Calendar.SECOND); NotepadMainFrame.label1.setText(" 当前时间:" + hour + ":" + min + ":" + second); try { Thread.sleep(1000); } catch (InterruptedException exception) { } } } }
    在NotepadMainFrame类中
    JToolBar toolState = new JToolBar();toolState.setSize(textArea.getSize().width, 10);//toolState.setLayout(new FlowLayout(FlowLayout.LEFT));toolState = new JToolBar(); toolState.setSize(textArea.getSize().width, 10);//toolState.setLayout(new FlowLayout(FlowLayout.LEFT)); label1 = new JLabel(" 当前系统时间:" + hour + ":" + min + ":" + second+" "); toolState.add(label1);Clock clock=new Clock();clock.start();
    6.1.7 显示行数和列数label2 = new JLabel(" 第 " + linenum + " 行, 第 " + columnnum+" 列 "); toolState.add(label2); toolState.addSeparator();textArea.addCaretListener(new CaretListener() { //记录行数和列数 public void caretUpdate(CaretEvent e) { JTextArea editArea = (JTextArea)e.getSource(); try { int caretpos = editArea.getCaretPosition(); linenum = editArea.getLineOfOffset(caretpos); columnnum = caretpos - textArea.getLineStartOffset(linenum); linenum += 1; label2.setText(" 第 " + linenum + " 行, 第 " + (columnnum+1)+" 列 "); } catch(Exception ex) { }}});contentPane.add(toolState, BorderLayout.SOUTH);toolState.setVisible(false);toolState.setFloatable(false);

    6.1.8 撤销、返回初始化一个UndoManger,然后通过body.getDocument().addUndoableEditListener(undoMgr)这个方法,就可以把撤销管理器的监听器添加到TextArea中。需要调用撤销的时候就调用unoMgr.undo()方法。
    UndoManager undoMgr=new UndoManager();JtextArea textArea.getDocument().addUndoableEditListener(undoMgr); if(undoMgr.canUndo()){ undoMgr.undo();//撤销} if(undoMgr.canRedo()){ undoMgr.redo();//恢复}
    6.2 程序目录截图
    七、系统测试


    测试评价
    针对实现的记事本的功能模块,基本上达到了预定的要求。
    缺陷(未实现功能):

    右键的菜单响应功能中的从右到左,Unicode码,汉字重选
    保存的文件设置了颜色和字体,打开后还是默认大小
    打印功能还可以再完善,现在只能打印一页多点,具体的设置还不是很懂,等我再多学点
    将自动换行和状态显示默认添加
    1 评论 21 下载 2018-11-04 15:19:13 下载需要6点积分
  • 基于C++的教室管理系统

    1 需求分析
    核心需求:实现教室管理系统,完成对学校教室的维护、查询、预约、统计功能。程序拥有权限控制,分为管理员和普通用户
    管理员需求:查看某栋楼教室的概要信息、每个教室的详细信息(包括教室基本信息、预约情况、上课情况)、修改某教室基本信息、增加教室、删除教室、增加某教室上课情况、统计某类教室的综合使用情况(周上课率、周预约率)
    普通用户需求:查询符合条件的空闲教室、预约空闲教室
    其他需求:程序具有良好的交互性

    2 概要设计程序运用面向对象编程思想进行设计。核心有教室类、预约信息类、上课信息类。

    教室类:

    成员:校区号、教学楼号、教室号、教室类型、座位数、预约状态、预约数、预约信息、上课数、上课信息
    函数:默认构造函数、成员获取函数、成员设置函数

    预约信息类:

    成员:课时时间、学号/教工号、姓名、电话
    函数:默认构造函数、构造函数、成员获取函数、重载流运算符

    上课信息类:

    成员:教师姓名、教工号、总上课次数、上课时间
    函数:默认构造函数、构造函数、成员获取函数、重载流运算符

    课时时间类:

    成员:课时周、周、节
    函数:重载相等、不等、流运算符


    程序结构图如下图所示:

    3 运行环境
    硬件环境

    处理器:Intel® Core™ i7-6500U CPU @ 2.50GHz 2.59GHz内存:8.00GB 系统类型:64位操作系统,基于x64的处理器
    软件环境:

    Windows 10

    4 开发工具和编程语言
    开发工具:

    Microsoft Visual Studio Community 2017
    编程语言:

    C++11

    5 详细设计5.1 教室信息类该类封装有教室的基本属性和方法,用于存储每个教室的信息集合及修改其中的信息,构成教室管理系统的核心。
    class Classroom{public: Classroom() = default; //默认构造函数 void setCampusType(uint); //设置函数 void setBuildID(uint); void setRoomID(uint); void setRoomType(uint); void setSeats(uint); void setOrderStatus(uint); void setOrderCnt(uint); void setClassCnt(uint); void addOrderInfo(OrderInfo); void addClassInfo(ClassInfo); uint getCampusType() const; //获取函数 uint getBuildID() const; uint getRoomID() const; uint getRoomType() const; uint getSeats() const; uint getOrderStatus() const; uint getOrderCnt() const; uint getClassCnt() const; vector <OrderInfo> getOrderInfo() const; vector <ClassInfo> getClassInfo() const;private: uint m_campusType = 0; //校区 uint m_buildID = 0; //楼号 uint m_roomID = 0; //教室号 uint m_roomType = 0; //教室类型 uint m_seats = 0; //座位数 uint m_orderStatus = 0; //预约状态 uint m_orderCnt = 0; //预约数 uint m_classCnt = 0; //课堂数 vector <OrderInfo> m_orderInfo; //预约信息数组 vector <ClassInfo> m_classInfo; //课堂信息数组};
    5.2 预约信息类主要用来存取每间教室的一条预约信息,含有预约时间、预约账号、预约者姓名及电话等基本信息,提供构造函数进行初始化,提供获取函数用于获取私有成员。
    class OrderInfo{ friend ostream& operator<< (ostream &, const OrderInfo&);public: OrderInfo() = default; OrderInfo(const classTime &time, const string &ID, const string &name, const string &phoneNum) : m_time(time), m_ID(ID), m_name(name), m_phoneNum(phoneNum) { } string getID() const; classTime getClassTime() const; string getName() const; string getPhoneNum() const;private: classTime m_time; string m_ID = ""; string m_name = ""; string m_phoneNum = "";};
    5.3上课信息类用来存取当前教室的一条上课信息,如上课时间、上课老师姓名、ID。
    class ClassInfo{ friend ostream& operator<< (ostream &, const ClassInfo &);public: ClassInfo() = default; ClassInfo(const string &teacherName, const string &teacherID, uint sumClassCnt, vector< classTime > &classTime) :m_teacherName(teacherName),m_teacherID(teacherID),m_sumClassCnt(sumClassCnt), m_classTime(classTime) {} string getName() const; string getID() const; uint getSumClassCnt() const; vector<classTime> getClassTime() const;private: string m_teacherName = ""; string m_teacherID = ""; uint m_sumClassCnt = 0; vector< classTime > m_classTime; //三元组 (课时周,周,节)};
    5.4 查看信息查看每座楼的所有教室基本信息,如教室号、教室类型、座位数、预约数、课程数。查看每个教室的详细信息,包括基本信息、预约信息、上课信息。

    修改信息:

    对某个教室的信息进行修改
    修改教室:

    可增加、删除一个教室
    增加管理员:

    增加一个管理员账号并存储
    增加上课信息:

    为某个教室增加上课信息
    统计使用情况:

    用于统计各类教室的使用率情况
    查询空闲教室:

    普通用户可通过输入课时时间来查询某号楼所有的空闲教室,同时可按照座位数、教室类型进行筛选
    预约空闲教室:

    普通用户可通过输入课时时间来预约教室、如果预约时间该教室有课或已有预约将预约失败

    6 调试分析
    问题1:程序所有功能都集中一起

    解决:增加用户权限,分为管理员和普通用户。管理员需要密码登陆,并对其进行校验,可对教室信息进行直接管理维护。对于普通用户仅开放预约及查询教室功能
    问题2:预约无校验功能

    解决:为系统添加时间属性、采用课时时间的方式对预约信息、上课信息进行时间管理。预约时可检测是否当前时间已被上课或预约占用
    问题3:交互逻辑使用不便

    解决:改进交互逻辑,提供跳转功能、选择错误处理

    程序改进

    改进管理员对教室进行维护的操作逻辑,使得操作更加方便
    增加真实时间、自动对已过期的预约进行删除
    采用更为合理的信息管理方式、使教室信息管理更加高效易行
    增加用户更多功能,如查询自己的预约、删除自己的预约
    增加程序鲁棒性
    改进交互

    7 测试结果管理员功能列表

    查询教室详细信息

    普通用户界面

    查询空闲教室

    预约空闲教室(预约成功)

    预约空闲教室(预约失败)

    参考文献[1] H.M.Deitel.C++大学基础教程(第五版)[M]. 北京: 电子工业出版社, 2011
    [2] 沈学东等. C++面向对象程序设计实用教程[M]. 上海: 上海交通大学出版社,2012
    [3] 谭浩强等. C++程序设计 [M]. 北京: 清华大学出版社, 2011
    [4] Stanley B.Lippman等. C++ Primer中文版[M]. 北京:电子工业出版,2017
    [5] Bjarne Stroustrup. C++程序设计语言[M]. 北京:机械工业出版社,2016
    1 评论 58 下载 2018-11-04 14:36:23 下载需要3点积分
  • 基于汇编语言实现的带小数的四则运算

    一 需求分析从键盘输入一个简单的表达式,如“ S=4+6*9-1+8/5”,按回车键结束输入,则屏幕显示S=58.6,小数点保留1位。假设输入的表达式中只含个位十进制数和 “+”、“-”、“*”、“/”运算符,且同一运算符最多出现2次。
    二 程序设计2.1 设计思想这个程序应该能正确处理数字和数学表达式的输入。我的设想是使其进一步处理最多12位十进制小数的输入,以及带有括号、四则运算算式的正确处理,并给出可以精确到小数点后五位的正确结果。
    该程序应完成工作:

    公式的输入,包括处理数字输入、符号输入,以及正确处理输入公式的句法
    公式的计算。其中包括正确处理各种符号运算的优先级和结合性、中间数的临时保存、小数的正确处理等
    结果的正确显示

    主程序的大致框图如下:

    2.2 PARSEEXP逐字符进行读取,并根据读取到的字符判断算式中出现的token属于什么类型。若是数字,则调用INPUTDECIMAL将其处理成双精度浮点数;若是运算符,则对应处理(见下)。
    对于算式的处理和运算,采用了调度场算法(Shunting yard algorithm)。采用两个堆栈,一个放数字,一个放运算符。当算法执行到将运算符放置到数字上的步骤时,立即进行运算。这样,分析完算式后,数字栈顶便是结果。
    运算用Intel处理器的FLD、FADD等指令完成。
    流程见下图,省略了对部分错误状况的处理,详见源码。

    2.3 INPUTDECIMAL利用了英特尔处理器的FBLD指令,将十进制的小数转换为浮点数。
    FBLD指令可以将一段长度为10字节空间中的前9字节中的18位BCD码从十进制整数转换为FPU寄存器中的REAL10浮点数,后一字节最高位决定符号位。
    如一系列从低位到高位排列的字节:21 43 65 87 00 00 00 00 00 80可以转换为-87654321.0,存储在FPU的栈顶。
    对字符串倒序、去掉小数点转BCD码后,用FBLD转换为大浮点数;再根据小数点位置,用FBLD构造一个是10的若干次幂的浮点数,除前者,得到字符串表示的浮点数。再用FSTP将其作为双精度数存储于内存,该功能即得以实现。
    大致框图见下图。

    2.4 OUTPUTDECIMAL利用的是FBLD的逆向指令FBSTP。将双精度数用FLD载入FPU的寄存器后,乘以100000,用FBSTP转换为包含小数点后五位的BCD数码,再从右往左解析输出,在第五位加小数点即可。
    三 程序实现共四个源码文件:

    MAIN.ASM
    ID.ASM
    OD.ASM
    PARSEEXP.ASM

    3.1 处理十进制小数的转换:ID.ASM
    包含过程:INPUTDECIMAL,负责将内存中的十进制小数字符串转换为双精度浮点数,仍然存在内存中
    输入参数包括字符串的起始位置DS:SI,和输出位置ES:BX

    3.2 处理十进制小数的输出:OD.ASM
    包含过程:OUTPUTDECIMAL,负责将内存中的双精度浮点数以十进制小数形式输出
    输入参数包括字符串的起始位置DS:SI

    3.3 处理算式:PARSEEXP.ASM
    包含过程:PARSEEXP,负责将内存中的一串算式字符串进行解析运算,并将最终结果存在内存中
    也同时输出算式的逆波兰符号形式,方便调试找出错误
    会调用ID.ASM、OD.ASM中的功能
    输入参数包括字符串的起始位置DS:SI和输出位置DS:BX

    3.4 主程序:MAIN.ASM
    包含过程MAIN,是程序的入口
    功能包括显示输入提示、让用户输入算式,以及调用PARSEEXP处理算式,最后用OUTPUTDECIMAL输出计算结果

    四 运行测试4.1 程序环境、适用范围本程序应运行于:

    Intel 的16位处理器(或模拟其工作的模拟器上),如8086,并且应支持浮点运算功能
    MS-DOS,或Windows 7及以下x86操作系统的命令提示符中(在NTVDM中运行)

    本程序的测试环境为:

    DOSBox,以及VMWare Workstation下的Windows 7 x86虚拟机的cmd.exe
    本程序适用于:

    简单的、输入式的、包括+、-、*、/、()的数学运算式,结果精度不超过5位小数
    4.2 使用方法
    启动MS-DOS操作系统(或DOSBox),或Windows x86系统的命令提示符(CMD.exe)
    用cd命令和盘符命令,将当前路径定位到本程序可执行文件的目录下
    输入MAIN.EXE,按回车键
    此时程序提示用户输入“S=”。用户应该输入一个算式,之后按回车键
    程序应能给出S的最终结果

    4.3 测试数据




    1 评论 25 下载 2018-11-04 10:29:53 下载需要4点积分
  • 基于汇编实现的贪吃蛇游戏

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