【计网】自定义协议与序列化(一) —— Socket封装于服务器端改写
🌎 应用层自定义协议与序列化
文章目录:
Tcp协议Socket编程
应用层简介
序列化和反序列化
重新理解read/write/recv/send及tcp的全双工
Socket封装
服务器端改写
🚀应用层简介
我们程序员写的一个个解决我们实际问题,满足我们日常需求的网络程序, 都是在应用层。
不论是Udp Socket编程还是Tcp Socket编程,所传的数据都是字符串类型的的数据,但是如果我们想要传输结构化的数据呢?什么是结构化的数据?其实在我们第一次说计算机网络时就已经提到过,结构化的数据就是协议,其本质就是 双方约定好的结构化的数据。
比如,我们如果要实现一个网络版的计算器,我们需要客户把待计算的两个数发过去,由服务器进行计算,最后把结果返回给客户端。如果我们依旧采用传统的Socket编程,不论是Tcp还是Udp Socket编程,都无法保证所收到的数据是完整的,比如:客户端要发送 123 * 123,但是服务器此时缓冲区快满了,只能收到 123 * 1,这个时候服务器端就会以 123 * 1作为客户端的请求进行处理,如此以来就会导致客户端想得到的结果不匹配,更有甚者,剩下的23后一批发来会不会导致新的客户端数据出现问题呢?
所以这里我们准备了两套方案:
方案一:
客户端发送一个形如"1+1"的字符串,这个字符串中有两个操作数,都是整形,两个数字之间会有一个字符是运算符,运算符只能是 + ,数字和运算符之间没有空格。
方案二:
定义结构体来表示我们需要交互的信息,发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体。这个过程我们叫做 序列化 和 反序列化。
🚀序列化和反序列化
首先先简单理解一下什么是序列化与反序列化,我们可以通过下图简单了解:
仅仅是上面一张图我认为理解还是不够的,要更好的理解序列化和反序列化需要从下面入手:
✈️重新理解read/write/recv/send及tcp的全双工
在重新理解这些接口之前,我们先来回顾一下进程向发消息到磁盘的过程:
首先,用户需要发送消息,那么OS就会从文件描述符表中把3号文件描述符通过进程pcb返回给用户,用户此时通过write()接口对文件进行写数据,字符串会从write接口通过文件描述符表找到struct file对象,从而找到内核缓冲区,将字符串拷贝到缓冲区当中。随后缓冲区就会定期的向磁盘文件中刷新数据。
这个我们在系统部分已经说过了,但是为什么要说这个呢?实际上,如果今天,我们把磁盘这个外设换为网络,实际上就变成了网络通信!我们之前说过,传输层和网络层协议属于操作系统内核的一部分! 如果今天主机想要通过网络进行Tcp协议通信,那么 在传输层OS会维护两个缓冲区,一个 发送缓冲区,一个 接收缓冲区:
在应用层,我们以双方约定好的协议,将数据进行序列化处理成一批字符串。我们前面在使用Socket编程的时候,直观上,我们都认为是send/sendto直接将数据发送给了对端,recv/recvfrom直接从对端接收数据。实际上双方的IO系统调用并不会直接作用于网络。如果是发送端,则调用write/send/sendto 接口发送到传输层的发送缓冲区,所以 write/send 本质是拷贝函数。将待发送的数据拷贝到发送缓冲区。
而发送数据则是由 Tcp 协议自主决定如何发送数据,而Tcp通过网络向对端发送数据,实际上就是 将自己发送缓冲区的内容通过网络拷贝到对方的接收缓冲区当中!所以 在传输层看来,是双方的操作系统在进行通信! 随后,对端的接收缓冲区就会通过 read/recv 等接口将数据拷贝到应用层,所以 read/recv 接口本质也是拷贝函数!最后将序列化的字符串交给上层,上层再根据协议进行反序列化,最终拿到相应的数据。
如果对端接收缓冲区内没有数据,那么 read/recv 接口就会阻塞等待,为什么会阻塞等待?因为缓冲区里没数据,而 本质上是因为调用read/recv接口的进程在等待数据的到来才会做下一步动作,从而将进程状态从运行态转变为阻塞态,当收到数据的时候再从阻塞态转为运行态。同样,如果主机A通过write/send 接口没有数据需要发送,也会阻塞等待。如果我们单单看发送方,有人把数据往发送缓冲区内写,OS把发送缓冲区的内容发送走,这难道不就是一个简单的生产消费者模型吗?生产者是用户,消费者是OS,交易场所是发送缓冲区。同样对于接收端来说,OS将数据通过网络拷贝到了接收缓冲区,上层用户需要通过 read/recv 来取出数据,那么对于接收方来说,也是一个生产消费者模型,只不过 生产者为 OS, 消费者为用户,交易场所为接收缓冲区。
由上面的例子,你认为 阻塞的本质是什么?是在进行同步! 为什么会是同步?因为通信双方需要等待数据的发送或者接收,而他们接收的过程无非就是发送端将数据拷贝到发送缓冲区,tcp再通过网络将将数据拷贝到对方的接收缓冲区中,对端用户需要调用 read/recv 拷贝接收缓冲区的数据到应用层。由此观之,通信的本质就是拷贝! 那么我们主机 A 在通过发送缓冲区给主机B的接收缓冲区发消息的时候,主机B不也可以通过自己的发送缓冲区给主机A的接收缓冲区发消息吗?它们之间互不干扰,所以这就是Tcp支持全双工的原因! 在Socket编程中我们说,一个 sockfd 也是支持全双工的,也是因为,sockfd既可以向发送缓冲区中发数据,也可以从接收缓冲区中拷贝数据!
✈️Socket封装
我们对Socket进行封装,使其以后无论是tcp协议还是udp协议,变得更加简洁,更加有条理性,这是因为创建一个套接字的方式方法是比较套路化的,所以我们可以对其进行封装,我们把创建套接字,绑定ip和端口,网络监听,网络接收,发起链接,分别封装为五个 纯虚函数,将来让子类进行强制重写:
class Socket
{
public:virtual void CreateSocketOrDie() = 0; // 创建套接字virtual void BindSocketOrDie(InetAddr &addr) = 0; // 绑定套接字virtual void ListenSocketOrDie() = 0; // 监听套接字virtual socket_sptr Accepter(InetAddr *addr) = 0;virtual bool Connector(InetAddr &addr) = 0;public:
};
如果我们想要创建服务器端与客户端的tcp socket通信,我们只需要调用不同的虚函数即可,当然也可以通过这些虚函数来组成udp socket通信,不过这里我们就不再实现udp的socket了:
class Socket
{
public:virtual void CreateSocketOrDie() = 0; // 创建套接字virtual void BindSocketOrDie(InetAddr &addr) = 0; // 绑定套接字virtual void ListenSocketOrDie() = 0; // 监听套接字virtual socket_sptr Accepter(InetAddr *addr) = 0; // 接收链接virtual bool Connector(InetAddr &addr) = 0; // 发起链接public:void BuildListenSocket(InetAddr &addr)// 创建tcp套接字,绑定并监听{CreateSocketOrDie();BindSocketOrDie(addr);ListenSocketOrDie();}bool BuildClientSocket(InetAddr &addr)// 创建客户端套接字{CreateSocketOrDie();return Connector(addr);}
};
那么,我们如果想要建立Tcp连接,我们就可以在TcpSocket类当中继承Socket类,这样我们就可以对基类成员虚函数进行重写,而重写的所有内容实际上就是我们之前写的TcpSocket的内容,这里我就不再过多赘述:
const static int gbacklog = 8;
using socket_sptr = std::shared_ptr<Socket>;// 重命名 Socket 类型的智能指针enum
{SOCKET_ERROR = 1,BIND_ERROR,LISTEN_ERROR,USAGE_ERROR,
};class TcpSocket : public Socket
{
public:TcpSocket(int fd) : _sockfd(fd){}void CreateSocketOrDie() override{// 创建链式套接字_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0){LOG(FATAL, "socket error");exit(SOCKET_ERROR);}LOG(DEBUG, "socket create success, sockfd is: %d", _sockfd);}void BindSocketOrDie(InetAddr &addr) override{struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(addr.Port());local.sin_addr.s_addr = inet_addr(addr.IP().c_str());int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local));if (n < 0){LOG(FATAL, "bind error");exit(BIND_ERROR);}LOG(DEBUG, "bind success, sockfd is: %d", _sockfd);}void ListenSocketOrDie() override{int n = ::listen(_sockfd, gbacklog);if (n < 0){LOG(FATAL, "listen error");exit(LISTEN_ERROR);}LOG(DEBUG, "listen success, sockfd is: %d", _sockfd);}socket_sptr Accepter(InetAddr *addr) override// 返回一个智能指针,以便于将来可以通过智能指针对基类成员方法进行调用{struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = ::accept(_sockfd, (struct sockaddr *)&peer, &len);if (sockfd < 0){LOG(WARNNING, "accept error");return nullptr;}// accpet成功*addr = peer;socket_sptr sock = std::make_shared<TcpSocket>(sockfd);return sock;}bool Connector(InetAddr &addr) override{struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(addr.Port());server.sin_addr.s_addr = inet_addr(addr.IP().c_str());int n = connect(_sockfd, (struct sockaddr *)&server, sizeof(server));if (n < 0){std::cerr << "connect error" << std::endl;return false;}return true;}private:int _sockfd;
};
这样写的好处是main函数和客户端的工作量就降低了很多,我们可以通过多态式的调用来完成Socket通信:
// 多态式调用
std::unique_ptr<Socket> listensock = std::make_unique<TcpSocket()>;
listensock->BuildListenSocket();// 直接建立起了连接std::unique_ptr<Socket> clientsock = std::make_unique<TcpSocket()>;
clientsock->BuildClientSocket();// 客户端Socket套接字建立
这样,listensock或者clientsock虽然表面调用的是Socket基类,但是由于基类内的纯虚函数都在子类实现,所以会间接调用子类对父类纯虚函数的重写,这就是多态式调用。而以上对于Socket的封装,内置抽象函数(纯虚函数),需要子类强制重新实现的这种方式,是一种设计模式,称为 模版方法模式。
✈️服务器端改写
除此之外,我们把Socket进行了封装,那么TcpServer也就不需要像Tcp Socket编程那样进行写了,为了松耦合,我们把TcpServer类冗余部分全部删除,TcpServer帮我们的目的是:创建套接字,获取客户端链接,再去处理请求 三个部分,至于如何处理请求,就不该是TcpServer类所关心的了。
首先,我们不再需要原本的初始化部分,因为我们对Socket进行了封装,我们只需要在TcpServer构造函数中进行调用即可:
TcpServer(int port): _localaddr("0", port)// 0表示接收任意地址, _listensock(std::make_unique<TcpSocket>())// 子类对象构造父类指针,以便于多态式调用, _isrunning(false)
{_listensock->BuildListenSocket(_localaddr);// 多态式调用, 构建了ListenSocket, 一步到位
}private:InetAddr _localaddr;std::unique_ptr<Socket> _listensock;bool _isrunning;
一个Socket的智能指针就可以实现多态式调用进行初始化Tcp套接字部分,对于具体的任务是如何处理的虽然TcpServer不该关心,但是如何分配任务以及分配任务的方式是我们需要操心的,在上一篇文章中我们有四种分配方式,后面三种选择任何一种都可,在这里我选择多线程的方式分配任务。
不需要知道具体的任务细节,只需要TcpServer其提供一个可调用的任务接口即可,我们使用function将Service接口封装为一个新的类型 io_service_t类型,在初始化部分与线程回调函数部分我们都可以对其进行调用:
using namespace socket_ns;
// socket_ns exists: using socket_sptr = std::shared_ptr<Socket>;class TcpServer;// 对任务进行封装
using io_service_t = std::function<void (socket_sptr sockfd, InetAddr client)>;class ThreadData
{
public:ThreadData(socket_sptr fd, InetAddr addr, TcpServer* s):sockfd(fd),clientaddr(addr),self(s){}
public:socket_sptr sockfd;// using socket_sptr = std::shared_ptr<Socket>;InetAddr clientaddr;TcpServer *self;
};// TcpServer的目的是为了创建套接字,获取链接,再去处理,具体如何处理,就不需要TcpServer关心了
class TcpServer
{
public:TcpServer(int port, io_service_t service): _localaddr("0", port), _listensock(std::make_unique<TcpSocket>()), _service(service), _isrunning(false){_listensock->BuildListenSocket(_localaddr);}// 线程回调函数static void* HandlerSock(void* args){pthread_detach(pthread_self());ThreadData* td = static_cast<ThreadData*>(args);td->self->_service(td->sockfd, td->clientaddr);delete td;return nullptr;}void Loop(){_isrunning = true;// 不能直接收数据,必须先获取连链接while(_isrunning){InetAddr peeraddr;socket_sptr normalsock = _listensock->Accepter(&peeraddr);// // using socket_sptr = std::shared_ptr<Socket>;if(normalsock == nullptr) continue;pthread_t t;ThreadData *td = new ThreadData(normalsock, peeraddr, this);pthread_create(&t, nullptr, HandlerSock, td);// 将线程分离}_isrunning = false;}~TcpServer(){}
private:InetAddr _localaddr;// 本地ip + portstd::unique_ptr<Socket> _listensock;bool _isrunning;io_service_t _service;
};
在Loop函数中,由于在TcpServer中,我们重写了Accept()方法,所以我们不需要在Loop中写裸的accept()原生接口了,我们直接使用_listensock进行调用Accept()方法,会返回一个TcpSocket的对象,对象中本就存在sockfd。这样我们就可以成功获取连接了,再将其交给线程去处理即可。
相关文章:
【计网】自定义协议与序列化(一) —— Socket封装于服务器端改写
🌎 应用层自定义协议与序列化 文章目录: Tcp协议Socket编程 应用层简介 序列化和反序列化 重新理解read/write/recv/send及tcp的全双工 Socket封装 服务器端改写 🚀应用层简介 我们程序员写的一个个解决…...
速度革命:esbuild如何改变前端构建游戏 (1)
什么是 esbuild? esbuild 是一款基于 Go 语言开发的 JavaScript 构建打包工具,以其卓越的性能著称。相比传统的构建工具(如 Webpack),esbuild 在打包速度上有着显著的优势,能够将打包速度提升 10 到 100 倍…...
大语言模型---什么是注意力机制?LlaMA 中注意力机制的数学定义
摘要 注意力机制(Attention Mechanism)是一种在深度学习和人工智能中广泛使用的技术,旨在使模型在处理信息时能够重点关注重要的部分,从而提升任务的效率和精度。它最初应用于自然语言处理(NLP)࿰…...
LSA详情与特殊区域
LSA是构成LSDB的重要原材料,在OSPF中发挥很大作用。 报文 通用头部 LS age:LSA寿命,0-3600s Options:可选项 LS type:LSA类型,三要素之一 Link State ID:LSAID 三要素之一 Advertising Ro…...
Python爬虫能处理动态加载的内容吗?
Python爬虫确实可以处理动态加载的内容。动态加载的内容通常是通过JavaScript在客户端执行,这意味着当网页首次加载时,服务器返回的HTML可能并不包含最终用户看到的内容。相反,JavaScript代码会在页面加载后从服务器请求额外的数据࿰…...
Spring Boot Web应用开发:数据访问
数据访问是Web应用的关键部分,Spring Boot简化了这一流程,特别是通过集成Java Persistence API (JPA) 来实现数据持久化。以下是如何在Spring Boot中配置数据源、使用JPA进行数据持久化以及创建访问数据的REST接口。 配置数据源 在Spring Boot中&#…...
【Linux】进程控制-----进程创建与进程终止
目录 前言: 一、进程创建: 1、fork函数 2、创建多个进程: 3、写时拷贝: 二、进程终止: 进程退出码: 退出方式: 编辑 进程异常退出: 缓冲区: 前言࿱…...
【软考速通笔记】系统架构设计师③——信息安全技术基础知识
文章目录 一、前言二、信息安全基础知识2.1 信息安全的基本要求2.2 信息安全的范围2.3 网络安全表现2.4 安全措施包括 三、信息安全系统的组成框架3.1 技术体系:3.2 组织机构体系:3.3 管理体系 四、信息加解密技术4.1 对称密钥加密算法4.2 非对称密钥加密…...
AI安全:从现实关切到未来展望
近年来,人工智能技术飞速发展,从简单的图像识别到生成对话,从自动驾驶到医疗诊断,AI技术正深刻改变着我们的生活。然而,伴随着这些进步,AI的安全性和可控性问题也日益凸显。这不仅涉及技术层面的挑战&#…...
YOLO格式数据集介绍
yolo数据集 yolo数据集标注格式主要是 yolov5 项目需要用到。 标签使用txt文本进行保存。yolo的目录如下所示: dataset ├─images │ ├─train │ │ ├─ flip_mirror_himg0026393.jpg │ │ ├─ flip_mirror_himg0026394.jpg │ │ ├─ flip_…...
Doris 数据集成 LakeSoul
Doris 数据集成 LakeSoul 作为一种全新的开放式的数据管理架构,湖仓一体(Data Lakehouse)融合了数据仓库的高性能、实时性以及数据湖的低成本、灵活性等优势,帮助用户更加便捷地满足各种数据处理分析的需求,在企业的大数据体系中已经得到越来越多的应用。 在过去多个版本…...
Navicat 预览变更sql
需求 用了Flyway(数据库迁移工具)后,需要记录变更sql,所以要知道变更sql。 查看方式 Navicat提供了预览变更sql功能,右击表---->设计表,比如修改字段后,点击SQL预览标签页, 顺…...
深入理解下oracle 11g block组成
深层次说,oracle数据库的最少组成单位应该是块,一般默认情况下,oracle数据库的块大小是8kb,其中存储着我们平常所需的数据。我们在使用过程中,难免会疑问道:“oracle数据块中到底是怎样组成的,平…...
Qt Graphics View 绘图架构
Qt Graphics View 绘图架构 "QWGraphicsView.h" 头文件代码如下: #pragma once#include <QGraphicsView>class QWGraphicsView : public QGraphicsView {Q_OBJECTpublic:QWGraphicsView(QWidget *parent);~QWGraphicsView();protected:void mouseM…...
大数据-234 离线数仓 - 异构数据源 DataX 将数据 从 HDFS 到 MySQL
点一下关注吧!!!非常感谢!!持续更新!!! Java篇开始了! 目前开始更新 MyBatis,一起深入浅出! 目前已经更新到了: Hadoop࿰…...
零基础学安全--shell脚本学习(1)脚本创建执行及变量使用
目录 学习连接 什么是shell shell的分类 查看当前系统支持shell 学习前提 开始学习 第一种执行脚本方法 编辑 第二种执行脚本方法 第三种执行脚本方法 变量声明和定义 编辑 查看变量 删除变量 学习连接 声明! 学习视频来自B站up主 **泷羽sec** 有兴趣…...
C#对INI配置文件进行读写操作方法
#region 读写ini配置文件/// <summary>/// 对INI文件进行读写/// </summary>class INIHelper{/// <summary>/// 从INI文件中读取数据/// </summary>/// <param name"filePath">INI文件的全路径</param>/// <param name"…...
华为鸿蒙内核成为HarmonyOS NEXT流畅安全新基座
HDC2024华为重磅发布全自研操作系统内核—鸿蒙内核,鸿蒙内核替换Linux内核成为HarmonyOS NEXT稳定流畅新基座。鸿蒙内核具备更弹性、更流畅、更安全三大特征,性能超越Linux内核10.7%。 鸿蒙内核更弹性:元OS架构,性能安全双收益 万…...
请求响应(学习笔记)
请求响应 文章目录 请求响应请求Postman简单参数实体参数数组集合参数数组参数集合参数 日期参数JSON参数路径参数 响应响应数据统一响应结果 分层解耦三层架构分层解耦IOC & DI 入门IOC详解DI详解 请求响应: 请求(HttpServeltRequest):获取请求数据…...
JavaScript核心语法(5)
这篇文章讲一下ES6中的核心语法:扩展运算符和模块化。 目录 1.扩展运算符 数组中的扩展运算符 基本用法 合并数组 对象中的扩展运算符 基本用法 合并对象 与解构赋值结合使用 数组解构中的剩余元素 对象解构中的剩余属性 2.模块化 基本概念 1.扩展运算符…...
2024年第15届蓝桥杯C/C++组蓝桥杯JAVA实现
目录 第一题握手,这个直接从49累加到7即可,没啥难度,后面7个不握手就好了,没啥讲的,(然后第二个题填空好难,嘻嘻不会) 第三题.好数编辑 第四题0R格式 宝石组合 数字接龙 最后一题:拔河 第…...
MongoDB 和 Redis 是两种不同类型的数据库比较
MongoDB 和 Redis 是两种不同类型的数据库,设计目标和应用场景各有侧重,因此性能对比需要结合具体需求场景进行评估。 1. MongoDB 性能特点 类型: 文档型数据库(NoSQL)。适合场景: 复杂查询:支持丰富的查询语法和索引…...
CLIP-Adapter: Better Vision-Language Models with Feature Adapters 论文解读
abstract 大规模对比视觉-语言预训练在视觉表示学习方面取得了显著进展。与传统的通过固定一组离散标签训练的视觉系统不同,(Radford et al., 2021) 引入了一种新范式,该范式在开放词汇环境中直接学习将图像与原始文本对齐。在下游任务中,通…...
Spring Boot 开发环境搭建详解
下面安装spring boot的详细步骤,涵盖了从安装 JDK 和 Maven 到创建和运行一个 Spring Boot 项目的全过程。 文章目录 1. 安装 JDK步骤 1.1:下载 JDK步骤 1.2:安装 JDK步骤 1.3:配置环境变量 2. 安装 Maven步骤 2.1:下载…...
网络安全中的数据科学如何重新定义安全实践?
组织每天处理大量数据,这些数据由各个团队和部门管理。这使得全面了解潜在威胁变得非常困难,常常导致疏忽。以前,公司依靠 FUD 方法(恐惧、不确定性和怀疑)来识别潜在攻击。然而,将数据科学集成到网络安全中…...
安装数据库客户端工具
如果没有勾选下面的,可以运行下面的两个命令 红框为自带数据库 新建数据库 右键运行mysql文件,找到数据库,并刷新...
GoogleTest做单元测试
目录 环境准备GoogleTest 环境准备 git clone https://github.com/google/googletest.git说cmkae版本过低了,解决方法 进到googletest中 cmake CMakeLists.txt make sudo make installls /usr/local/lib存在以下文件说明安装成功 中间出了个问题就是,…...
深入解析 EasyExcel 组件原理与应用
✨深入解析 EasyExcel 组件原理与应用✨ 官方:EasyExcel官方文档 - 基于Java的Excel处理工具 | Easy Excel 官网 在日常的 Java 开发工作中,处理 Excel 文件的导入导出是极为常见的需求。 今天,咱们就一起来深入了解一款非常实用的操作 Exce…...
JSON数据转化为Excel及数据处理分析
在现代数据处理中,JSON(JavaScript Object Notation)因其轻量级和易于人阅读的特点而被广泛使用。然而,有时我们需要将这些JSON数据转化为Excel格式以便于进一步的分析和处理。本文将介绍如何将JSON数据转化为Excel文件࿰…...
(计算机网络)期末
计算机网络概述 物理层 信源就是发送方 信宿就是接收方 串行通信--一次只发一个单位的数据(串行输入) 并行通信--一次可以传输多个单位的数据 光纤--利用光的反射进行传输 传输之前,要对信源进行一个编码,收到信息之后要进行一个…...
建设商务网站的步骤/最近时事新闻热点事件
背景: 我的jira数据库中已有数据,想修改数据集,不能通过简单的修改字符集完成,需要先将原数据导出,经过适当调整后重新导入才可完成。 下面的步骤可以进行问题的解决(假设原字符集是latin1,想修…...
可以看网站的浏览器/2345网址大全下载到桌面
一、问题 在使用latex写英文论文时遇到一个问题,不知道如何输入藏文字符或者藏语字体,经过查询,实现了如何使用latex显示藏语文字,具体过程分享给大家参考。 二、实现步骤 导入两个包 \usepackage{fontspec} \setmainfont{Micro…...
raid管理网站开发/seo咨询服务价格
大家在编程过程中都会用到一些异步编程的情况。在c#的BCL中,很多api都提供了异步方法,初学者可能对各种不同异步方法的使用感到迷惑,本文主要为大家梳理一下异步方法的变迁以及如何使用异步方法。 BeginXXX,EndXXX模式 在.Net Fra…...
开发网站开票名称是什么/如何投放网络广告
3、评测平台介绍及方法说明AMD FM1(APU)平台CPU AMD A6-3650(4核/4线程)主板 华硕 F1A75-M PRO(A75)内存 宇瞻 DDR3-1600 2G x 2(8-8-8-24)硬盘 日立 1TB显卡 Radeon HD 6530D(APU内置)Radeon HD 6670 双显卡交火Radeon HD 6570 双显卡交火Intel LGA1155平台CPU Intel Core i3 …...
网站降权 垃圾外链/搜索引擎推广
全文转发 http://www.cnblogs.com/fnng/p/3576154.html 在我们日常上网浏览网页的时候,经常会看到一些好看的图片,我们就希望把这些图片保存下载,或者用户用来做桌面壁纸,或者用来做设计的素材。 我们最常规的做法就是通过鼠标右键…...
文化部网站总分馆建设实施意见/前端seo是什么
随着时代的发展,信息技术已经深深地渗透到人类的方方面面。现代信息技术已经开始改变人类的学习方式、思维方式和工作方式。现代的教育方式也由以前单一的形式向多元化发展,只有利用现代信息技术进行学习、探索和创造,才能提高教师的教研能力…...