参考:https://studygolang.com/pkgdoc

导入方法:

import "net/rpc"

RPC(Remote Procedure Call Protocol)就是想实现函数调用模式的网络化,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。

客户端就像调用本地函数一样,然后客户端把这些参数打包之后通过网络传给服务端,服务端解包到处理过程中执行,然后执行结果返回给客户端

运行时一次客户机对服务器的RPC调用步骤有:

  • 调用客户端句柄,执行传送参数
  • 调用本地系统内核发送网络信息
  • 消息传送到远程主机
  • 服务器句柄得到消息并取得参数
  • 执行远程过程
  • 执行的过程将结果返回服务器句柄
  • 服务器句柄返回结果,调用远程系统内核
  • 消息传回本地主机
  • 客户句柄由内核接收消息
  • 客户接收句柄返回的数据

Go标准包中已经提供了对RPC的支持,支持三个级别的RPC:TCP、HTTP、JSONRPC,下面将一一说明

Go的RPC包与传统的RPC系统不同,他只支持Go开发的服务器与客户端之间的交互,因为在内部,它们采用了Gob来编码

Go RPC的函数要满足下面的条件才能够被远程调用,不然会被忽略:

  • 函数必须是导出的,即首字母为大写
  • 必须有两个导出类型的参数
  • 第一个参数是接收的参数,第二个参数是返回给客户端的参数,第二个参数必须是指针类型的
  • 函数还要有一个error类型返回值。方法的返回值,如果非nil,将被作为字符串回传,在客户端看来就和errors.New创建的一样。如果返回了错误,回复的参数将不会被发送给客户端。

举个例子,正确的RPC函数格式为:

func (t *T) MethidName(argType T1, replyType *T2) error

T、T1和T2类型都必须能被encoding/gob包编解码

任何RPC都需要通过网络来传递数据,Go RPC可以利用HTTP和TCP来传递数据

Constants

const (
// HandleHTTP使用的默认值
DefaultRPCPath = "/_goRPC_"
DefaultDebugPath = "/debug/rpc"
)

Variables

var DefaultServer = NewServer()

DefaultServer是*Server的默认实例,本包和Server方法同名的函数都是对其方法的封装。

type Server

type Server struct {
// 内含隐藏或非导出字段
}

Server代表RPC服务端。

func NewServer

func NewServer() *Server

NewServer创建并返回一个*Server。

func (*Server) Register

func (server *Server) Register(rcvr interface{}) error

Register在server注册并公布rcvr的方法集中满足如下要求的方法:

- 方法是导出的
- 方法有两个参数,都是导出类型或内建类型
- 方法的第二个参数是指针
- 方法只有一个error接口类型的返回值

如果rcvr不是一个导出类型的值,或者该类型没有满足要求的方法,Register会返回错误。Register也会使用log包将错误写入日志。客户端可以使用格式为"Type.Method"的字符串访问这些方法,其中Type是rcvr的具体类型。

func (*Server) RegisterName

func (server *Server) RegisterName(name string, rcvr interface{}) error

RegisterName类似Register,但使用提供的name代替rcvr的具体类型名作为服务名。

func Register

func Register(rcvr interface{}) error

Register在DefaultServer注册并公布rcvr的方法。

其实就相当于调用NewServer函数生成一个*Server,然后再调用其的(*Server) Register函数

func HandleHTTP

func HandleHTTP()

HandleHTTP函数注册DefaultServer的RPC信息HTTP处理器对应到DefaultRPCPath,和DefaultServer的debug处理器对应到DefaultDebugPath。HandleHTTP函数会注册到http.DefaultServeMux。之后,仍需要调用http.Serve(),一般会另开线程:"go http.Serve(l, nil)"

其实就相当于调用NewServer函数生成一个*Server,然后再调用其的(*Server) HandleHTTP函数

func DialHTTP

func DialHTTP(network, address string) (*Client, error)

DialHTTP在指定的网络和地址与在默认HTTP RPC路径监听的HTTP RPC服务端连接。

func DialHTTPPath

func DialHTTPPath(network, address, path string) (*Client, error)

DialHTTPPath在指定的网络、地址和路径与HTTP RPC服务端连接。

上面两个函数都是通过HTTP的方式和服务器建立连接,之间的区别之在于是否设置上下文路径。

type Client

type Client struct {
codec ClientCodec reqMutex sync.Mutex // protects following
request Request mutex sync.Mutex // protects following
seq uint64
pending map[uint64]*Call
closing bool // user has called Close
shutdown bool // server has told us to stop
}

Client类型代表RPC客户端。同一个客户端可能有多个未返回的调用,也可能被多个go程同时使用。

func (*Client) Call

func (client *Client) Call(serviceMethod string, args interface{}, reply interface{}) error

Call调用指定的方法,等待调用返回,将结果写入reply,然后返回执行的错误状态。

有三个参数,第一个要写调用函数的名字,第二个是要传递的参数,第三个是要返回的参数(这个注意是指针类型)

举例:

HTTP RPC

利用HTTP的好处是可以直接复用net/http中的一些函数,下面举例说明:

服务端:

package main

import (
"fmt"
"net/http"
"net/rpc"
"errors"
) type Args struct{
A, B int
} type Quotient struct{
Quo, Rem int
} type Arith int func (t *Arith) Multiply(args *Args, reply *int) error{
*reply = args.A * args.B
return nil
} func (t *Arith) Divide(args *Args, quo *Quotient) error{
if args.B == 0{
return errors.New("divide by zero")
}
quo.Quo = args.A / args.B
quo.Rem = args.A % args.B
return nil
} func main() {
arith := new(Arith)
rpc.Register(arith)
rpc.HandleHTTP() err := http.ListenAndServe(":1234", nil)
if err != nil{
fmt.Println(err.Error())
}
}

客户端:

package main

import (
"fmt"
"net/rpc"
"log"
"os"
)
type Args struct{
A, B int
} type Quotient struct{
Quo, Rem int
} func main() {
if len(os.Args) != 2{
fmt.Println("Usage: ", os.Args[0], "server")
os.Exit(1)
}
serverAddress := os.Args[1] client, err := rpc.DialHTTP("tcp", serverAddress + ":1234")
if err != nil{
log.Fatal("dialing : ", err)
} //Synchronous call
args := Args{17, 8}
var reply int
err = client.Call("Arith.Multiply", args, &reply)
if err != nil{
log.Fatal("arith error : ", err)
}
fmt.Printf("Arith: %d*%d = %d \n", args.A, args.B, reply) var quot Quotient
err = client.Call("Arith.Divide", args, &quot)
if err != nil{
log.Fatal("arith error : ", err)
}
fmt.Printf("Arith: %d/%d = %d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
}

客户端返回:

userdeMBP:go-learning user$ go run test.go
Usage: /var/folders/2_/g5wrlg3x75zbzyqvsd5f093r0000gn/T/go-build438875911/b001/exe/test server
exit status 1
userdeMBP:go-learning user$ go run test.go 127.0.0.1
Arith: 17*8 = 136
Arith: 17/8 = 2 remainder

TCP RPC连接

func Dial

func Dial(network, address string) (*Client, error)

Dial在指定的网络和地址与RPC服务端连接。

func (*Server) ServeConn

func (server *Server) ServeConn(conn io.ReadWriteCloser)

ServeConn在单个连接上执行server。ServeConn会阻塞,服务该连接直到客户端挂起。调用者一般应另开线程调用本函数:"go server.ServeConn(conn)"。ServeConn在该连接使用gob(参见encoding/gob包)有线格式。要使用其他的编解码器,可调用ServeCodec方法。

func ServeConn

func ServeConn(conn io.ReadWriteCloser)

ServeConn在单个连接上执行DefaultServer。ServeConn会阻塞,服务该连接直到客户端挂起。调用者一般应另开线程调用本函数:"go ServeConn(conn)"。ServeConn在该连接使用gob(参见encoding/gob包)有线格式。要使用其他的编解码器,可调用ServeCodec方法。

其实就相当于调用NewServer函数生成一个*Server,然后再调用其的(*Server) ServeConn函数

JSON RPC连接

服务端:

package main

import (
"fmt"
"net"
"net/rpc"
"net/rpc/jsonrpc"
"errors"
"os"
) type Args struct{
A, B int
} type Quotient struct{
Quo, Rem int
} type Arith int func (t *Arith) Multiply(args *Args, reply *int) error{
*reply = args.A * args.B
return nil
} func (t *Arith) Divide(args *Args, quo *Quotient) error{
if args.B == 0{
return errors.New("divide by zero")
}
quo.Quo = args.A / args.B
quo.Rem = args.A % args.B
return nil
} func main() {
arith := new(Arith)
rpc.Register(arith) tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234")//jsonrpc是基于TCP协议的,现在他还不支持http协议
if err != nil{
fmt.Println(err.Error())
os.Exit(1)
} listener, err := net.ListenTCP("tcp", tcpAddr)
if err != nil{
fmt.Println(err.Error())
os.Exit(1)
}
for{
conn, err := listener.Accept()
if err != nil{
continue
}
jsonrpc.ServeConn(conn)
}
}

客户端:

package main

import (
"fmt"
"net/rpc/jsonrpc"
"log"
"os"
)
type Args struct{
A, B int
} type Quotient struct{
Quo, Rem int
} func main() {
if len(os.Args) != 2{
fmt.Println("Usage: ", os.Args[0], "server:port")
os.Exit(1)
}
service := os.Args[1] client, err := jsonrpc.Dial("tcp", service)
if err != nil{
log.Fatal("dialing : ", err)
} //Synchronous call
args := Args{17, 8}
var reply int
err = client.Call("Arith.Multiply", args, &reply)
if err != nil{
log.Fatal("arith error : ", err)
}
fmt.Printf("Arith: %d*%d = %d \n", args.A, args.B, reply) var quot Quotient
err = client.Call("Arith.Divide", args, &quot)
if err != nil{
log.Fatal("arith error : ", err)
}
fmt.Printf("Arith: %d/%d = %d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
}

客户端返回:

userdeMBP:go-learning user$ go run test.go 127.0.0.1:1234
Arith: 17*8 = 136
Arith: 17/8 = 2 remainder

type Request

type Request struct {
ServiceMethod string // 格式:"Service.Method"
Seq uint64 // 由客户端选择的序列号
// 内含隐藏或非导出字段
}

Request是每个RPC调用请求的头域。它是被内部使用的,这里的文档用于帮助debug,如分析网络拥堵时。

type Response

type Response struct {
ServiceMethod string // 对应请求的同一字段
Seq uint64 // 对应请求的同一字段
Error string // 可能的错误
// 内含隐藏或非导出字段
}

Response是每个RPC调用回复的头域。它是被内部使用的,这里的文档用于帮助debug,如分析网络拥堵时。

type ClientCodec

type ClientCodec interface {
// 本方法必须能安全的被多个go程同时使用
WriteRequest(*Request, interface{}) error
ReadResponseHeader(*Response) error
ReadResponseBody(interface{}) error
Close() error
}

ClientCodec接口实现了RPC会话的客户端一侧RPC请求的写入和RPC回复的读取。客户端调用WriteRequest来写入请求到连接,然后成对调用ReadRsponseHeader和ReadResponseBody以读取回复。客户端在结束该连接的事务时调用Close方法。ReadResponseBody可以使用nil参数调用,以强制回复的主体被读取然后丢弃。

func NewClient

func NewClient(conn io.ReadWriteCloser) *Client

NewClient返回一个新的Client,以管理对连接另一端的服务的请求。它添加缓冲到连接的写入侧,以便将回复的头域和有效负载作为一个单元发送。

func NewClientWithCodec

func NewClientWithCodec(codec ClientCodec) *Client

NewClientWithCodec类似NewClient,但使用指定的编解码器,以编码请求主体和解码回复主体。

NewClient使用默认编码gobClientCodec,NewClientWithCodec使用自定义的其它编码。

默认的gobClientCodec代码为:

type gobClientCodec struct {
rwc io.ReadWriteCloser
dec *gob.Decoder
enc *gob.Encoder
encBuf *bufio.Writer
}

//指定客户端将以什么样的编码方式将信息发送给服务端,在调用Go和Call函数是会自动使用WriteRequest方法发送信息
func (c *gobClientCodec) WriteRequest(r *Request, body interface{}) (err error) {
if err = c.enc.Encode(r); err != nil { //Encode方法将request r编码后发送
return
}
if err = c.enc.Encode(body); err != nil {//Encode方法将body编码后发送
return
}
return c.encBuf.Flush()
} func (c *gobClientCodec) ReadResponseHeader(r *Response) error {
return c.dec.Decode(r)////Decode从输入流读取下一个之并将该值存入response r
} func (c *gobClientCodec) ReadResponseBody(body interface{}) error {
return c.dec.Decode(body) //Decode从输入流读取下一个之并将该值存入body
} func (c *gobClientCodec) Close() error {
return c.rwc.Close()
}

创建Client时将调用默认成对调用ReadRsponseHeader和ReadResponseBody以读取回复

举例自定义编码:

package main

import (
"log"
"net/rpc"
// "errors"
"fmt"
)
type shutdownCodec struct {
responded chan int
closed bool
} func (c *shutdownCodec) WriteRequest(*rpc.Request, interface{}) error {//这是client用来发送请求的方法
fmt.Println("call WriteRequest")
return nil
}
func (c *shutdownCodec) ReadResponseBody(interface{}) error{
fmt.Println("call ReadResponseBody")
return nil
}
func (c *shutdownCodec) ReadResponseHeader(*rpc.Response) error {
c.responded <- //如果注释掉这里,则会一直卡在"wait response : "
return nil
// return errors.New("shutdownCodec ReadResponseHeader") //如果返回的是error,那么就不会去调用ReadResponseBody了
}
func (c *shutdownCodec) Close() error {
c.closed = true
return nil
} func main() {
codec := &shutdownCodec{responded: make(chan int)}
client := rpc.NewClientWithCodec(codec)
fmt.Println("wait response : ") //从返回结果可以看出来,NewClientWithCodec后会自动成对调用ReadResponseBody和ReadResponseHeader
fmt.Println(<-codec.responded)
fmt.Println(codec.closed) //false
client.Close()
if !codec.closed {
log.Fatal("client.Close did not close codec")
}
fmt.Println(codec.closed) //true
}

type ServerCodec

type ServerCodec interface {
ReadRequestHeader(*Request) error
ReadRequestBody(interface{}) error
// 本方法必须能安全的被多个go程同时使用
WriteResponse(*Response, interface{}) error
Close() error
}

ServerCodec接口实现了RPC会话的服务端一侧RPC请求的读取和RPC回复的写入。服务端通过成对调用方法ReadRequestHeader和ReadRequestBody从连接读取请求,然后调用WriteResponse来写入回复。服务端在结束该连接的事务时调用Close方法。ReadRequestBody可以使用nil参数调用,以强制请求的主体被读取然后丢弃。

默认的gobServerCodec:

type gobServerCodec struct {
rwc io.ReadWriteCloser
dec *gob.Decoder
enc *gob.Encoder
encBuf *bufio.Writer
closed bool
} func (c *gobServerCodec) ReadRequestHeader(r *Request) error {
return c.dec.Decode(r) //解码并读取client发来的请求request
} func (c *gobServerCodec) ReadRequestBody(body interface{}) error {
return c.dec.Decode(body) //解码并读取client发来的请求body
}

//指定服务端将会以什么样的编码方式将数据返回给客户端,在调用ServeRequest和ServeCodec方法时会自动调用该WriteResponse函数
func (c *gobServerCodec) WriteResponse(r *Response, body interface{}) (err error) {
if err = c.enc.Encode(r); err != nil { //对响应信息request编码
if c.encBuf.Flush() == nil {
// Gob couldn't encode the header. Should not happen, so if it does,
// shut down the connection to signal that the connection is broken.
log.Println("rpc: gob error encoding response:", err)
c.Close()
}
return
}
if err = c.enc.Encode(body); err != nil {//对响应信息body编码
if c.encBuf.Flush() == nil {
// Was a gob problem encoding the body but the header has been written.
// Shut down the connection to signal that the connection is broken.
log.Println("rpc: gob error encoding body:", err)
c.Close()
}
return
}
return c.encBuf.Flush()
} func (c *gobServerCodec) Close() error {
if c.closed {
// Only call c.rwc.Close once; otherwise the semantics are undefined.
return nil
}
c.closed = true
return c.rwc.Close()
}

func (*Server) ServeCodec

func (server *Server) ServeCodec(codec ServerCodec)

ServeCodec类似ServeConn,但使用指定的编解码器,以编码请求主体和解码回复主体。

func (*Server) ServeRequest

func (server *Server) ServeRequest(codec ServerCodec) error

ServeRequest类似ServeCodec,但异步的服务单个请求。它不会在调用结束后关闭codec。

下面两个函数的不同在于他们使用在DefaultServer上:

func ServeCodec

func ServeCodec(codec ServerCodec)

ServeCodec类似ServeConn,但使用指定的编解码器,以编码请求主体和解码回复主体。

func ServeRequest

func ServeRequest(codec ServerCodec) error

ServeRequest类似ServeCodec,但异步的服务单个请求。它不会在调用结束后关闭codec。

func (*Client) Close

func (client *Client) Close() error

func (*Server) Accept

func (server *Server) Accept(lis net.Listener)

Accept接收监听器l获取的连接,然后服务每一个连接。Accept会阻塞,调用者应另开线程:"go server.Accept(l)"

举例:

package main

import (
"log"
"net"
"net/rpc"
"strings"
"fmt"
)
type R struct {
// Not exported, so R does not work with gob.
// 所以这样运行的话会报错rpc: gob error encoding body: gob: type main.R has no exported fields
// msg []byte
Msg []byte //改成这样
} type S struct{} func (s *S) Recv(nul *struct{}, reply *R) error {
*reply = R{[]byte("foo")}
return nil
} func main() {
defer func() {
err := recover()
if err == nil {
log.Fatal("no error")
}
if !strings.Contains(err.(error).Error(), "reading body EOF") {
log.Fatal("expected `reading body EOF', got", err)
}
}()
//服务端
rpc.Register(new(S)) listen, err := net.Listen("tcp", "127.0.0.1:1234")//端口为0表示任意端口
if err != nil {
panic(err)
}
go rpc.Accept(listen) //必须用并发监听,因为客户端和服务端写在了一起 //客户端
client, err := rpc.Dial("tcp", listen.Addr().String())
if err != nil {
panic(err)
} var reply R
err = client.Call("S.Recv", &struct{}{}, &reply)
if err != nil {
panic(err)
} fmt.Printf("%q\n", reply) client.Close()
listen.Close() }

返回:

userdeMBP:go-learning user$ go run test.go
{"foo"}
// :: rpc.Serve: accept:accept tcp 127.0.0.1:: use of closed network connection
// :: no error
exit status

type Call

type Call struct {
ServiceMethod string // 调用的服务和方法的名称
Args interface{} // 函数的参数(下层为结构体指针)
Reply interface{} // 函数的回复(下层为结构体指针)
Error error // 在调用结束后,保管错误的状态
Done chan *Call // 对其的接收操作会阻塞,直到远程调用结束
}

Call类型代表一个执行中/执行完毕的RPC会话。

func (*Client) Go

func (client *Client) Go(serviceMethod string, args interface{}, reply interface{}, done chan *Call) *Call

Go异步的调用函数。本方法Call结构体类型指针的返回值代表该次远程调用。通道类型的参数done会在本次调用完成时发出信号(通过返回本次Go方法的返回值)。如果done为nil,Go会申请一个新的通道(写入返回值的Done字段);如果done非nil,done必须有缓冲,否则Go方法会故意崩溃。

举例:

使用的仍是上面TCP RPC的服务端,客户端改为:

package main

import (
"fmt"
"net/rpc"
"log"
"os"
)
type Args struct{
A, B int
} type Quotient struct{
Quo, Rem int
} func main() {
if len(os.Args) != {
fmt.Println("Usage: ", os.Args[], "server:port")
os.Exit()
}
service := os.Args[] client, err := rpc.Dial("tcp", service)
if err != nil{
log.Fatal("dialing : ", err)
} //Synchronous call
args := Args{, }
var reply int
done := make(chan *rpc.Call, ) call := client.Go("Arith.Multiply", args, &reply, done) //异步调用,只有当该方法执行完毕后done的值才不为nil
if call.Error != nil {
log.Fatal(err)
} if resultCall := <-done; resultCall != nil {//如果不<-done,reply将不会有结果,reply将为0
fmt.Printf("Arith: %d*%d = %d \n", args.A, args.B, reply)
fmt.Printf("done : %#v\n", resultCall)
fmt.Printf("Multiply result : %#v\n", *(resultCall.Reply.(*int)))//根据Call的Reply得到返回的值
} var quot Quotient
err = client.Call("Arith.Divide", args, &quot)
if err != nil{
log.Fatal("arith error : ", err)
}
fmt.Printf("Arith: %d/%d = %d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem) }

返回:

userdeMBP:go-learning user$ go run test.go 127.0.0.1:
Arith: * =
done : &rpc.Call{ServiceMethod:"Arith.Multiply", Args:main.Args{A:, B:}, Reply:(*int)(0xc000012100), Error:error(nil), Done:(chan *rpc.Call)(0xc0001142a0)}
Multiply result :
Arith: / = remainder

go标准库的学习-net/rpc的更多相关文章

  1. go标准库的学习-net/rpc/jsonrpc

    参考:https://studygolang.com/pkgdoc 导入方式: import "net/rpc/jsonrpc" jsonrpc包实现了JSON-RPC的Clien ...

  2. go标准库的学习-net/http

    参考:https://studygolang.com/pkgdoc 概念解释: request:用户请求的信息,用来解析用户的请求信息,包括post.get.cookie.url等信息 respons ...

  3. go标准库的学习-database/sql

    参考:https://studygolang.com/pkgdoc 导入方式: import "database/sql" sql包提供了保证SQL或类SQL数据库的泛用接口. 使 ...

  4. go标准库的学习-crypto/md5

    参考:https://studygolang.com/pkgdoc 导入方式: import "crypto/md5" md5包实现了MD5哈希算法,参见RFC 1321. Con ...

  5. go标准库的学习-crypto/sha1

    参考:https://studygolang.com/pkgdoc 导入方式: import "crypto/sha1" sha1包实现了SHA1哈希算法,参见RFC 3174. ...

  6. go标准库的学习-crypto/sha256

    参考:https://studygolang.com/pkgdoc 导入方式: import "crypto/sha256" sha256包实现了SHA224和SHA256哈希算法 ...

  7. python 标准库基础学习之开发工具部分1学习

    #2个标准库模块放一起学习,这样减少占用地方和空间#标准库之compileall字节编译源文件import compileall,re,sys#作用是查找到python文件,并把它们编译成字节码表示, ...

  8. python calendar标准库基础学习

    # -*- coding: utf-8 -*-# 作者:新手__author__ = 'Administrator'#标准库:日期时间基础学习:calendar:处理日期#例1import calen ...

  9. golang标准库分析之net/rpc

    net/rpc是golang提供的一个实现rpc的标准库.

随机推荐

  1. JavaScript字符串常用方法

    toUpperCase():把一个字符串全部变为大写 toLowerCase():把一个字符串全部变为小写 indexOf():会搜索制定字符串出现的位置,有返回索引,没有返回-1 substring ...

  2. LeetCode翻转矩阵后的得分-Python3<六>

    上一篇:LeetCode子域名访问计数-Python3.7<五> 题目:https://leetcode-cn.com/problems/score-after-flipping-matr ...

  3. C#---初学ActiveMQ中间件

    本篇文章只适合跟我一样的初学者,因为现阶段的我们只想者怎么实现功能,不太会去考虑潜在异常.从上周开始优化公司的调控系统,原先采取的都是通过操作数据库去实现功能,客户体验效果不佳,经过领导决定是用中间件 ...

  4. MySQL分库分表浅谈

    一.分库分表类型 1.单库单表 所有数据都放在一个库,一张表. 2.单库多表 数据在一个库,单表水平切分多张表. 3.多库多表 数据库水平切分,表也水平切分. 二.分库分表查询 通过分库分表规则查找到 ...

  5. MEF 插件式开发之 DotNetCore 初体验

    背景叙述 在传统的基于 .Net Framework 框架下进行的 MEF 开发,大多是使用 MEF 1,对应的命名空间是 System.ComponentModel.Composition.在 Do ...

  6. sql server: quering roles, schemas, users,logins

    --https://docs.microsoft.com/en-us/sql/relational-databases/security/authentication-access/managing- ...

  7. 【代码笔记】Web-HTML-基础

    一,效果图. 二,代码. <!DOCTYPE html> <html> <head> <meta charset="utf-8"> ...

  8. 【读书笔记】iOS-个人开发者与企业开发者的区别

    个人开发者与企业开发者的一个主要的区别在于独立开发者授权描述文件必须列出具体的设备.另一个不同就是开发者账户最多使用100台设备,而企业则可以让苹果公司生成未锁定到特定设备并可以安装到任何设备上的授权 ...

  9. 从零开始学习html(二)认识标签(第一部分)——下

    八.<blockquote>标签,长文本引用 <!DOCTYPE HTML> <html> <head> <meta http-equiv=&qu ...

  10. sqlserver配置实践

    对于一套新的sqlserver服务器,我们首先要对它做一些必要的优化配置,确保在生产上比较长的时间段内可以比较稳定的,良好的运行. 新的sqlserver服务器上安装的sqlserver版本,可以选择 ...