Unity3D网络游戏实战——通用服务器框架_架设本地c# unity游戏服务器-程序员宅基地

技术标签: unity  3d  Unity  服务器  网络编程  

前言

网络游戏涉及客户端和服务端。服务端程序记录玩家数据,处理客户端发来的协议。本文就介绍一套通用客户端的实现。
该框架基于Select多路复用处理网络消息,具有粘包半包处理、心跳机制等功能,还是用MySQL数据库存储玩家数据,是一套功能较完备的C#服务端程序。一般单个服务端进程可以承载数百名玩家,如果更多就需要改为分布式架构。

7.1服务端架构

服务端两大核心是处理客户端的消息和存储玩家数据。
客户端与服务端通过TCP连接,使两者可以传递数据,服务端还连接着MySQL数据库,可将玩家数据保存到数据库中。

7.1.2模块划分

  • 逻辑层:消息处理,事件处理,存储结构。
  • 网络底层:连接客户端,消息处理,事件处理
  • 数据库底层:连接数据库,存储结构。

7.2Json编码解码

和客户端不同的是,因为服务端程序和Unity无关,无法使用JsonUtility,所以改用System.Web提供的方法实现。(需要手动引用System.web.Extensions)
在这里插入图片描述

新建一个控制台程序,将协议文件全部放在proto下。

7.2.3修改MsgBase

引入System.Web.Script.Serialization头文件后,和JsonUtility调用方法不同。因为JavaScriptSerializer不是静态类,需要定义一个该类型的对象,再让对象调用Serialize和Deserialize方法进行编码解码

7.3网络模块

7.3.1整体架构

  • 协议解析
  • 处理select多路复用的网络管理器NetManager(核心)
  • 定义客户端信息ClientState类
  • 处理网络消息的MsgHandler类,但是把不同消息类型的处理分拆到多个文件中。(BattleMsgHandler处理战斗协议、SysMsgHandler处理ping、pong协议)
  • 事件处理类EventHandler

7.3.2ClientState

客户端信息,每一个客户端连接对应一个ClientState,含有与客户端连接的套接字socket和读缓冲区readBuff,以及对应的玩家数据和最后一次收到ping的时间。

using System.Net.Sockets;

public class ClientState
{
    
	public Socket socket; 
	public ByteArray readBuff = new ByteArray(); 
	//Ping
	public long lastPingTime = 0;
	//玩家
	public Player player;
}

7.3.3开启监听和多路复用

客户端和服务端的NetManager功能相似,都是处理链接、粉发消息和网络事件。
但是为了管理多个连接,服务端采用了多路复用技术。

public static void StartLoop(int listenPort)
	{
    
		//Socket
		listenfd = new Socket(AddressFamily.InterNetwork,
			SocketType.Stream, ProtocolType.Tcp);
		//Bind
		IPAddress ipAdr = IPAddress.Parse("0.0.0.0");
		IPEndPoint ipEp = new IPEndPoint(ipAdr, listenPort);
		listenfd.Bind(ipEp);
		//Listen
		listenfd.Listen(0);
		Console.WriteLine("[服务器]启动成功");
		//循环
		while (true)
		{
    
			ResetCheckRead();  //重置checkRead
			Socket.Select(checkRead, null, null, 1000);
			//检查可读对象
			for (int i = checkRead.Count - 1; i >= 0; i--)
			{
    
				Socket s = checkRead[i];
				if (s == listenfd)
				{
    
					ReadListenfd(s);
				}
				else
				{
    
					ReadClientfd(s);
				}
			}
			//超时
			Timer();
		}
	}

服务端开启了端口监听后,进入循环。针对Select返回的列表,程序遍历它判断有新的客户端连接还是某个客户端发来消息,然后分别调用处理函数ReadListenfd和ReadClientfd。
当程序在Select有可读事件和超时都会调用Timer,空闲状态每秒调用一次。

处理监听消息以及处理客户端消息和前面写的都差不多,就不详细介绍了。

7.4心跳机制

我们在前面的clientstate已经加入了lastpingtime,注意是long,游戏客户端只运行几小时,unity提供的Time.time即可记录。但是服务端可能运行纪念,所以要用long保存。

7.4.2时间戳

时间戳是一种记录时间的方法,也就是1970年1月1日零点到现在的秒数。

//获取时间戳
	public static long GetTimeStamp()
	{
    
		TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
		return Convert.ToInt64(ts.TotalSeconds);
	}

7.4.3回应MsgPing协议

更新lastPingTime并回应

using System;

public partial class MsgHandler
{
    
    public static void MsgPing(ClientState c,MsgBase msgBase)
    {
    
        Console.WriteLine("MsgPing");
        c.lastPingTime = NetManager.GetTimeStamp();
        MsgPong msgPong = new MsgPong();
        NetManager.Send(c, msgPong);
    }
}

7.4.4超时处理

遍历客户端连接,太久没收到就断开连接,并删除clients列表元素。注意这是在遍历clients,删除后再遍历会出错,所以直接break。每次checkping最多断开一个连接。
在ontimer中调用,ontimer是在timer里通过反射调用,timer在startloop里调用

public static void OnTimer()
    {
    
        CheckPing();
    }

    //Ping检查
    public static void CheckPing()
    {
    
        //现在的时间戳
        long timeNow = NetManager.GetTimeStamp();
        //遍历,删除
        foreach (ClientState s in NetManager.clients.Values)
        {
    
            if (timeNow - s.lastPingTime > NetManager.pingInterval * 4)
            {
    
                Console.WriteLine("Ping Close " + s.socket.RemoteEndPoint.ToString());
                NetManager.Close(s);
                return;
            }
        }
    }

7.5玩家的数据结构

7.5.2Player

我们需要Player和PlayerData,Player里有id、socket,playerdata(存入数据库),临时坐标(无需存入数据库)

public class PlayerData
{
    
    //金币
    public int coin = 0;
    //记事本
    public string text = "new text";
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

public class Player
{
    
    //id
    public string id = "";
    //指向ClientState
    public ClientState state;
    //临时数据,如:坐标
    public int x;
    public int y;
    public int z;
    //数据库数据
    public PlayerData data;

    //construct
    public Player(ClientState state)
    {
    
        this.state = state;
    }
    //发送消息
    public void Send(MsgBase msgBase)
    {
    
        NetManager.Send(state, msgBase);
    }
}相互引用

7.5.3PlayerManager

用id作为player(字典)的key来获取player,而不是ip+port,因为随时会变。
NetManager.clients保存所有客户端信息(ClientState),PlayerManager.players保存所有玩家对象(player),客户端信息通过clientState.player引用玩家对象,玩家对象通过player.state引用客户端信息,两者相互引用配合。

7.6配置MySQL数据库

两部分

  1. 安装和启动MySQL服务器,让它监听某个端口
  2. 使用第三方库编码和解码MySQL特定形式的协议

7.6.1安装并启动MySQL数据库

这里安装配置好的集成环境xampp

XAMPP是apache 、mysql、PHP 的集成的web 服务器软件

简单地说就是个数据库服务器!
安装完成后点击MySQL后方的Start按钮即可。
在这里插入图片描述

7.6.2Navicat for MySQL

这是一套专为MySQL数据库服务的管理工具,可以用它建立数据库并查看数据表的内容,比直接使用MySQL的命令行语句方便。
安装好点击连接,新建连接,填入MySQL数据库的IP、端口用户名和密码,登陆数据库。
在这里插入图片描述
在这里我们自己创建game库,下面包含account和player两个表,分别记录id和password以及id和playerdata(都将键长度为20的id作为主键)。账号和游戏数据分开的好处是,一个账号可对应多个游戏的数据。

7.6.4安装connector

为解析MySQL的网络数据,我们需要引用connector这个第三方库,可以直接从书中资源下载。
添加MySql.Data.dll和System.Data.dll的依赖。

7.7数据库模块

在服务端编写DbManager.cs,用于处理数据库相关事务。提供从数据库读取玩家数据、将玩家数据保存到数据库、注册、检测密码是否正确等功能。
因为connector的.Net Framework版本是4.5.2,所以我们需要将服务端程序的框架也改为这个,防止引用不到MySql.Data。
在这里插入图片描述

在这里插入图片描述

7.7.1连接数据库

连接MySQL第一步就是发起对数据库的网络连接。connector封装了所有与数据库交互的方法。
在引用"MySql.Data.MySqlClient"后,新建一个MySQL连接对象,设置数据库、用户名、密码后调用Open即可发起连接。
该连接对象和socket相似,我们将数据库名、数据库IP、数据库端口号及用户名密码等组装成ConnectionString所需的格式再调用open即可。

public static MySqlConnection mysql;
    static JavaScriptSerializer Js = new JavaScriptSerializer();//用于调用一些序列化方法的对象

    //连接mysql数据库
    public static bool Connect(string db,string ip,int port,string user,string pw)
    {
    
        //创建mysqlconnection对象
        mysql = new MySqlConnection();
        //连接参数
        string s = string.Format("Database={0}; Data Source={1}; port={2}; User Id={3}; Password={4}", db, ip, port, user, pw);
        mysql.ConnectionString = s;
        //连接
        try
        {
    
            mysql.Open();
            Console.WriteLine("[数据库] Connect succ");
            return true;
        }
        catch(Exception e)
        {
    
            Console.WriteLine("[数据库] Connect fail" + e.Message);
            return false;
        }
    }

先开启数据库服务器,再开启服务端程序,就可以显示连接成功。

7.1.2防止SQL注入

SQL注入,就是通过输入请求,把SQL命令插入到SQL语句中,已达到欺骗服务期执行恶意SQL命令的目的。比如取一个"xiaoming;delete * from player;"的名字,这样就会在操作这条用户名的时候删除player表。
所以为了防止SQL注入,我们把含有逗号、分号等特殊字符的字符串判定为不安全字符串,在拼装SQL语句前就进行安全监测。

using System.Text.RegularExpressions;
//判定安全字符串
    private static bool IsSafeString(string str)
    {
    
        return !Regex.IsMatch(str, @"[-|;|,|\/|\(|\)|\[|\]|\}|]{|%|@|\*|!\']");
    }

7.7.3IsAccountExist

注册账号时判断账号是否已存在,不能再次注册。MySqlDataReader提供遍历数据集的方法,HasRows指明数据及是否包含数据。

//是否存在该用户
    public static bool IsAccountExist(string id)
    {
    
        //防SQL注入
        if (!DbManager.IsSafeString(id))
        {
    
            return false;
        }
        //SQL语句
        string s = string.Format("select * from account where id='{0}';", id);
        //查询
        try
        {
    
            MySqlCommand cmd = new MySqlCommand(s, mysql);
            MySqlDataReader dataReader = cmd.ExecuteReader();
            bool hasRows = dataReader.HasRows;
            dataReader.Close();
            return !hasRows;
        }
        catch(Exception e)
        {
    
            Console.WriteLine("[数据库] IsSafeString err," + e.Message);
            return false;
        }
    }

7.7.4Register

通过insert into account set id=***, pw=***向account表插入数据。
但是一般不会吧明文的password存入数据库,而是先加密,比如加上md5加密,这样数据库被盗仍然不能从加密的密码获取用户信息。

 //注册
    public static bool Register(string id,string pw)
    {
    
        //防SQL注入
        if (!DbManager.IsSafeString(id))
        {
    
            Console.WriteLine("[数据库] Register fail, id not safe");
            return false;
        }
        if (!DbManager.IsSafeString(pw))
        {
    
            Console.WriteLine("[数据库] Register fail, pw not safe");
            return false;
        }
        //能否注册
        if (!IsAccountExist(id))
        {
    
            Console.WriteLine("[数据库] Register fail, id exist");
            return false;
        }
        //写入数据库User表
        string sql = string.Format("insert into account set id ='{0}',pw = '{1}';", id, pw);
        try
        {
    
            MySqlCommand cmd = new MySqlCommand(sql, mysql);
            cmd.ExecuteNonQuery();
            return true;
        }
        catch(Exception e)
        {
    
            Console.WriteLine("[数据库] Register fail" + e.Message);
            return false;
        }
    }

7.7.5CreatePlayer

1.将默认的PlayerData对象序列化成Json数据
2.将数据保存到player表的data栏位中

//创建角色
    public static bool CreatePlayer(string id)
    {
    
        //防sql注入
        if (!DbManager.IsSafeString(id))
        {
    
            Console.WriteLine("[数据库] CreatePlayer fail, id not safe");
            return false;
        }
        //序列化
        PlayerData playerData = new PlayerData();
        string data = Js.Serialize(playerData);
        //写入数据库
        string sql = string.Format("insert into player set id = '{0}',data = '{1}';", id, data);
        try
        {
    
            MySqlCommand cmd = new MySqlCommand(sql, mysql);
            cmd.ExecuteNonQuery();
            return true;
        }
        catch(Exception e)
        {
    
            Console.WriteLine("[数据库] CreatePlayer err," + e.Message);
            return false;
        }
    }

7.7.6CheckPassword

通过select * from account where id=*** and pw= ***查询数据库,如果有数据dataReader.HasRows==true说明id和pw正确。

7.7.7GetPlayerData

读取玩家数据,通过id在player表中搜寻数据,player表以id为key,以字符串的形式存放着序列化后的Json数据。
先用dataReader获取到对应账号的玩家数据,再将字符串反序列化PlayerData对象返回。

//创建角色
    public static bool CreatePlayer(string id)
    {
    
        //防sql注入
        if (!DbManager.IsSafeString(id))
        {
    
            Console.WriteLine("[数据库] CreatePlayer fail, id not safe");
            return false;
        }
        //序列化
        PlayerData playerData = new PlayerData();
        string data = Js.Serialize(playerData);
        //写入数据库
        string sql = string.Format("insert into player set id = '{0}',data = '{1}';", id, data);
        try
        {
    
            MySqlCommand cmd = new MySqlCommand(sql, mysql);
            cmd.ExecuteNonQuery();
            return true;
        }
        catch(Exception e)
        {
    
            Console.WriteLine("[数据库] CreatePlayer err," + e.Message);
            return false;
        }
    }

7.7.8UpdatePlayerData

将玩家数据playerData序列化成字符串,然后用形如"update player set data="{“coin”:100}" where id= “lpy”;"的SQL语句更新数据库中的数据

//保存角色
    public static bool UpdatePlayerData(string id, PlayerData playerData)
    {
    
        //序列化
        string data = Js.Serialize(playerData);
        //sql
        string sql = string.Format("update player set data='{0}' where id ='{1}';", data, id);
        //更新
        try
        {
    
            MySqlCommand cmd = new MySqlCommand(sql, mysql);
            cmd.ExecuteNonQuery();
            return true;
        }
        catch(Exception e)
        {
    
            Console.WriteLine("[数据库] UpdatePlayerData err, " + e.Message);
            return false;
        }
    }

7.8登录注册功能

接下来用一个记事本跑通前面的流程。

7.8.1注册登陆协议

LoginMsg中包含了注册、登陆和踢出三条协议,均为服务端回应客户端的,一般有result和reason说明成功与否或者失败的原因

using System;
using System.Collections.Generic;

//注册
public class MsgRegister : MsgBase
{
    
    public MsgRegister() {
     protoName = "MsgRegister"; }
    //客户端发,协议内容
    public string id = "";
    public string pw = "";
    //服务端回(0-成功,1-失败)
    public int result = 0;
}

//登陆
public class MsgLogin : MsgBase
{
    
    public MsgLogin() {
     protoName = "MsgLogin"; }
    //客户端发
    public string id = "";
    public string pw = "";
    //服务端回(0-成功,1-失败)
    public int result = 0;
}

//踢下线(服务端推送)
public class MsgKick : MsgBase
{
    
    public MsgKick() {
     protoName = "MsgKick"; }
    //原因(0-其他人登陆同一账号)
    public int reason = 0;
}

7.8.3注册功能

添加LoginMsgHandle.cs,在其中编写MsgHandler(partial),先注册,再创建角色。

//注册协议处理
    public static void MsgRegister(ClientState c, MsgBase msgBase)
    {
    
        MsgRegister msg = (MsgRegister)msgBase;
        //注册
        if (DbManager.Register(msg.id, msg.pw))
        {
    
            DbManager.CreatePlayer(msg.id);
            msg.result = 0;
        }
        else
        {
    
            msg.result = 1;
        }
        NetManager.Send(c, msg);
    }

7.8.4登陆功能

  1. 验证密码
  2. 状态判断(是否已经登陆)
  3. 踢下线,后上的踢掉先上的
  4. 读取playerData构建player对象,并加入到PlayerManager的列表中
//登陆协议处理
    public static void MsgLogin(ClientState c,MsgBase msgBase)
    {
    
        MsgLogin msg = (MsgLogin)msgBase;
        //密码校验
        if (!DbManager.CheckPassword(msg.id, msg.pw))
        {
    
            msg.result = 1;
            NetManager.Send(c, msg);
            return;
        }
        //不允许再次登陆
        if(c.player != null)
        {
    
            msg.result = 1;
            NetManager.Send(c, msg);
            return;
        }
        //如果已经登陆,踢下线
        if (PlayerManager.IsOnline(msg.id))
        {
    
            //发送踢下线协议
            Player other = PlayerManager.GetPlayer(msg.id);
            MsgKick msgKick = new MsgKick();
            msgKick.reason = 0;
            other.Send(msgKick);
            //断开连接
            NetManager.Close(other.state);
        }
        //获取玩家数据
        PlayerData playerData = DbManager.GetPlayerData(msg.id);
        if(playerData == null)
        {
    
            msg.result = 1;
            NetManager.Send(c, msg);
            return;
        }
        //构建player
        Player player = new Player(c);
        player.id = msg.id;
        player.data = playerData;
        PlayerManager.AddPlayer(msg.id, player);
        c.player = player;
        //返回协议
        msg.result = 0;
        player.Send(msg);
    }

7.8.9客户端监听

先把服务端的两个协议文件复制到客户端中,在start进行协议的监听!

void Start()
    {
    
        NetManager.AddEventListener(NetManager.NetEvent.ConnectSucc, OnConnectSucc);
        NetManager.AddEventListener(NetManager.NetEvent.ConnectFail, OnConnectFail);
        NetManager.AddEventListener(NetManager.NetEvent.Close, OnConnectClose);
        NetManager.AddMsgListener("MsgMove", OnMsgMove);
        NetManager.AddMsgListener("MsgRegister", OnMsgRegister);
        NetManager.AddMsgListener("MsgLogin", OnMsgLogin);
        NetManager.AddMsgListener("MsgKick", OnMsgKick);
        NetManager.AddMsgListener("MsgGetText", OnMsgGetText);
        NetManager.AddMsgListener("MsgSaveText", OnMsgSaveText);
    }

7.8.10注册功能

//收到服务端发送的注册协议后,自动调用的回调方法
    public void OnMsgRegister(MsgBase msgBase)
    {
    
        MsgRegister msg = (MsgRegister)msgBase;
        if(msg.result == 0)
        {
    
            Debug.Log("注册成功");
        }
        else
        {
    
            Debug.Log("注册失败");
        }
    }

    //发送注册协议
    public void OnRegisterClick()
    {
    
        MsgRegister msg = new MsgRegister();
        msg.id = idInput.text;
        msg.pw = pwInput.text;
        NetManager.Send(msg);
    }

7.8.11登陆

//收到服务端发送的注册协议后,自动调用的回调方法
    public void OnMsgRegister(MsgBase msgBase)
    {
    
        MsgRegister msg = (MsgRegister)msgBase;
        if(msg.result == 0)
        {
    
            Debug.Log("注册成功");
        }
        else
        {
    
            Debug.Log("注册失败");
        }
    }

    //发送注册协议
    public void OnRegisterClick()
    {
    
        MsgRegister msg = new MsgRegister();
        msg.id = idInput.text;
        msg.pw = pwInput.text;
        NetManager.Send(msg);
    }
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/J_avaSmallWhite/article/details/121219337

智能推荐

FTP命令字和返回码_ftp 登录返回230-程序员宅基地

文章浏览阅读3.5k次,点赞2次,收藏13次。为了从FTP服务器下载文件,需要要实现一个简单的FTP客户端。FTP(文件传输协议) 是 TCP/IP 协议组中的应用层协议。FTP协议使用字符串格式命令字,每条命令都是一行字符串,以“\r\n”结尾。客户端发送格式是:命令+空格+参数+"\r\n"的格式服务器返回格式是以:状态码+空格+提示字符串+"\r\n"的格式,代码只要解析状态码就可以了。读写文件需要登陆服务器,特殊用..._ftp 登录返回230

centos7安装rabbitmq3.6.5_centos7 安装rabbitmq3.6.5-程序员宅基地

文章浏览阅读648次。前提:systemctl stop firewalld 关闭防火墙关闭selinux查看getenforce临时关闭setenforce 0永久关闭sed-i'/SELINUX/s/enforcing/disabled/'/etc/selinux/configselinux的三种模式enforcing:强制模式,SELinux 运作中,且已经正确的开始限制..._centos7 安装rabbitmq3.6.5

idea导入android工程,idea怎样导入Android studio 项目?-程序员宅基地

文章浏览阅读5.8k次。满意答案s55f2avsx2017.09.05采纳率:46%等级:12已帮助:5646人新版Android Studio/IntelliJ IDEA可以直接导入eclipse项目,不再推荐使用eclipse导出gradle的方式2启动Android Studio/IntelliJ IDEA,选择 import project3选择eclipse 项目4选择 create project f..._android studio 项目导入idea 看不懂安卓项目

浅谈AI大模型技术:概念、发展和应用_ai大模型应用开发-程序员宅基地

文章浏览阅读860次,点赞2次,收藏6次。AI大模型技术已经在自然语言处理、计算机视觉、多模态交互等领域取得了显著的进展和成果,同时也引发了一系列新的挑战和问题,如数据质量、计算效率、知识可解释性、安全可靠性等。城市运维涉及到多个方面,如交通管理、环境监测、公共安全、社会治理等,它们需要处理和分析大量的多模态数据,如图像、视频、语音、文本等,并根据不同的场景和需求,提供合适的决策和响应。知识搜索有多种形式,如语义搜索、对话搜索、图像搜索、视频搜索等,它们可以根据用户的输入和意图,从海量的数据源中检索出最相关的信息,并以友好的方式呈现给用户。_ai大模型应用开发

非常详细的阻抗测试基础知识_阻抗实部和虚部-程序员宅基地

文章浏览阅读8.2k次,点赞12次,收藏121次。为什么要测量阻抗呢?阻抗能代表什么?阻抗测量的注意事项... ...很多人可能会带着一系列的问题来阅读本文。不管是数字电路工程师还是射频工程师,都在关注各类器件的阻抗,本文非常值得一读。全文13000多字,认真读完大概需要2小时。一、阻抗测试基本概念阻抗定义:阻抗是元器件或电路对周期的交流信号的总的反作用。AC 交流测试信号 (幅度和频率)。包括实部和虚部。​图1 阻抗的定义阻抗是评测电路、元件以及制作元件材料的重要参数。那么什么是阻抗呢?让我们先来看一下阻抗的定义。首先阻抗是一个矢量。通常,阻抗是_阻抗实部和虚部

小学生python游戏编程arcade----基本知识1_arcade语言 like-程序员宅基地

文章浏览阅读955次。前面章节分享试用了pyzero,pygame但随着想增加更丰富的游戏内容,好多还要进行自己编写类,从今天开始解绍一个新的python游戏库arcade模块。通过此次的《连连看》游戏实现,让我对swing的相关知识有了进一步的了解,对java这门语言也有了比以前更深刻的认识。java的一些基本语法,比如数据类型、运算符、程序流程控制和数组等,理解更加透彻。java最核心的核心就是面向对象思想,对于这一个概念,终于悟到了一些。_arcade语言 like

随便推点

【增强版短视频去水印源码】去水印微信小程序+去水印软件源码_去水印机要增强版-程序员宅基地

文章浏览阅读1.1k次。源码简介与安装说明:2021增强版短视频去水印源码 去水印微信小程序源码网站 去水印软件源码安装环境(需要材料):备案域名–服务器安装宝塔-安装 Nginx 或者 Apachephp5.6 以上-安装 sg11 插件小程序已自带解析接口,支持全网主流短视频平台,搭建好了就能用注:接口是公益的,那么多人用解析慢是肯定的,前段和后端源码已经打包,上传服务器之后在配置文件修改数据库密码。然后输入自己的域名,进入后台,创建小程序,输入自己的小程序配置即可安装说明:上传源码,修改data/_去水印机要增强版

verilog进阶语法-触发器原语_fdre #(.init(1'b0) // initial value of register (1-程序员宅基地

文章浏览阅读557次。1. 触发器是FPGA存储数据的基本单元2. 触发器作为时序逻辑的基本元件,官方提供了丰富的配置方式,以适应各种可能的应用场景。_fdre #(.init(1'b0) // initial value of register (1'b0 or 1'b1) ) fdce_osc (

嵌入式面试/笔试C相关总结_嵌入式面试笔试c语言知识点-程序员宅基地

文章浏览阅读560次。本该是不同编译器结果不同,但是尝试了g++ msvc都是先计算c,再计算b,最后得到a+b+c是经过赋值以后的b和c参与计算而不是6。由上表可知,将q复制到p数组可以表示为:*p++=*q++,*优先级高,先取到对应q数组的值,然后两个++都是在后面,该行运算完后执行++。在电脑端编译完后会分为text data bss三种,其中text为可执行程序,data为初始化过的ro+rw变量,bss为未初始化或初始化为0变量。_嵌入式面试笔试c语言知识点

57 Things I've Learned Founding 3 Tech Companies_mature-程序员宅基地

文章浏览阅读2.3k次。57 Things I've Learned Founding 3 Tech CompaniesJason Goldberg, Betashop | Oct. 29, 2010, 1:29 PMI’ve been founding andhelping run techn_mature

一个脚本搞定文件合并去重,大数据处理,可以合并几个G以上的文件_python 超大文本合并-程序员宅基地

文章浏览阅读1.9k次。问题:先讲下需求,有若干个文本文件(txt或者csv文件等),每行代表一条数据,现在希望能合并成 1 个文本文件,且需要去除重复行。分析:一向奉行简单原则,如无必要,绝不复杂。如果数据量不大,那么如下两条命令就可以搞定合并:cat a.txt >> new.txtcat b.txt >> new.txt……去重:cat new...._python 超大文本合并

支付宝小程序iOS端过渡页DFLoadingPageRootController分析_类似支付宝页面过度加载页-程序员宅基地

文章浏览阅读489次。这个过渡页是第一次打开小程序展示的,点击某个小程序前把手机的开发者->network link conditioner->enable & very bad network 就会在停在此页。比如《支付宝运动》这个小程序先看这个类的.h可以看到它继承于DTViewController点击左上角返回的方法- (void)back;#import "DTViewController.h"#import "APBaseLoadingV..._类似支付宝页面过度加载页

推荐文章

热门文章

相关标签