主要内容

1. Tcp编程
2. redis使用

1. Tcp编程

(1)简介

      Golang是谷歌设计开发的语言,在Golang的设计之初就把高并发的性能作为Golang的主要特性之一,也是面向大规模后端服务程序。在服务器端网络通信是必不可少的也是至关重要的一部分。Golang内置的包例如net、net/http中的底层就是对TCP socket方法的封装。

TCP简介:

 Golang是谷歌设计开发的语言,在Golang的设计之初就把高并发的性能作为Golang的主要特性之一,也是面向大规模后端服务程序。在服务器端网络通信是必不可少的也是至关重要的一部分。Golang内置的包例如net、net/http中的底层就是对TCP socket方法的封装。
网络编程方面,我们最常用的就是tcp socket编程了,在posix标准出来后,socket在各大主流OS平台上都得到了很好的支持。关于tcp programming,最好的资料莫过于W. Richard Stevens 的网络编程圣经《UNIX网络 编程 卷1:套接字联网API》 了,书中关于tcp socket接口的各种使用、行为模式、异常处理讲解的十分细致。

TCP简介

      Go是自带runtime的跨平台编程语言,Go中暴露给语言使用者的tcp socket api是建立OS原生tcp socket接口之上的。由于Go runtime调度的需要,golang tcp socket接口在行为特点与异常处理方面与OS原生接口有着一些差别。

(2)模型

      从tcp socket诞生后,网络编程架构模型也几经演化,大致是:“每进程一个连接” –> “每线程一个连接” –> “Non-Block + I/O多路复用(linux epoll/windows iocp/freebsd darwin kqueue/solaris Event Port)”。伴随着模型的演化,服务程序愈加强大,可以支持更多的连接,获得更好的处理性能

目前主流web server一般均采用的都是”Non-Block + I/O多路复用”(有的也结合了多线程、多进程)。不过I/O多路复用也给使用者带来了不小的复杂度,以至于后续出现了许多高性能的I/O多路复用框架, 比如libevent、libev、libuv等,以帮助开发者简化开发复杂性,降低心智负担。不过Go的设计者似乎认为I/O多路复用的这种通过回调机制割裂控制流 的方式依旧复杂,且有悖于“一般逻辑”设计,为此Go语言将该“复杂性”隐藏在Runtime中了:Go开发者无需关注socket是否是 non-block的,也无需亲自注册文件描述符的回调,只需在每个连接对应的goroutine中以“block I/O”的方式对待socket处理即可,这可以说大大降低了开发人员的心智负担。一个典型的Go server端程序大致如下:

 //go-tcpsock/server.go
func HandleConn(conn net.Conn) {
defer conn.Close() for {
// read from the connection
// ... ...
// write to the connection
//... ...
}
} func main() {
listen, err := net.Listen("tcp", ":8888")
if err != nil {
fmt.Println("listen error: ", err)
return
} for {
conn, err := listen.Accept()
if err != nil {
fmt.Println("accept error: ", err)
break
} // start a new goroutine to handle the new connection
go HandleConn(conn)
}
}

Go server编程

     (重点)用户层眼中看到的goroutine中的“block socket”,实际上是通过Go runtime中的netpoller通过Non-block socket + I/O多路复用机制“模拟”出来的,真实的underlying socket实际上是non-block的,只是runtime拦截了底层socket系统调用的错误码,并通过netpoller和goroutine 调度让goroutine“阻塞”在用户层得到的Socket fd上。比如:当用户层针对某个socket fd发起read操作时,如果该socket fd中尚无数据,那么runtime会将该socket fd加入到netpoller中监听,同时对应的goroutine被挂起,直到runtime收到socket fd 数据ready的通知,runtime才会重新唤醒等待在该socket fd上准备read的那个Goroutine。而这个过程从Goroutine的视角来看,就像是read操作一直block在那个socket fd上似的。

关于netpoller可以看下这为博主博客:http://www.opscoder.info/golang_netpoller.html

(3)TCP连接的建立

      众所周知,TCP Socket的连接的建立需要经历客户端和服务端的三次握手的过程。连接建立过程中,服务端是一个标准的Listen + Accept的结构(可参考上面的代码),而在客户端Go语言使用net.Dial()或net.DialTimeout()进行连接建立。

服务端的处理流程: a. 监听端口     b. 接收客户端的链接     c. 创建goroutine,处理该链接

客户端的处理流程: a. 建立与服务端的链接     b. 进行数据收发     c. 关闭链接

通过客户端可服务端实现一个简单的聊天系统?

客户端:

 package main

 import (
"bufio"
"fmt"
"net"
"os"
"strings"
) func main() {
fmt.Println("start client...")
conn, err := net.Dial("tcp", "localhost:50000")
if err != nil {
fmt.Println("Error dialing", err.Error())
return
} defer conn.Close()
inputReader := bufio.NewReader(os.Stdin)
for {
input, _ := inputReader.ReadString('\n')
trimmedInput := strings.Trim(input, "\r\n")
if trimmedInput == "Q" {
return
}
_, err = conn.Write([]byte(trimmedInput))
if err != nil {
return
}
}
}

client.go

服务端:

 package main

 import (
"fmt"
"net"
"io"
)
func main() {
fmt.Println("start server...")
listen, err := net.Listen("tcp", "0.0.0.0:50000")
if err != nil {
fmt.Println("listen failed, err:", err)
return
}
for {
conn, err := listen.Accept()
if err != nil {
fmt.Println("accept failed, err:", err)
continue
}
go process(conn)
}
} func process(conn net.Conn) {
defer conn.Close() for {
buf := make([]byte, )
_, err := conn.Read(buf) if err == io.EOF { //当客户端断开的时候就无法读到数据
fmt.Println("read end")
return
} if err != nil {
fmt.Println("read err:", err)
return
}
fmt.Println("read: ", string(buf))
}
}

server.go

     阻塞Dial:

 conn, err := net.Dial("tcp", "www.baidu.com:80")
if err != nil {
//handle error
}
//read or write on conn

阻塞Dial

      超时机制的Dial:

 conn, err := net.DialTimeout("tcp", "www.baidu.com:80", *time.Second)
if err != nil {
//handle error
}
//read or write on conn

超时Dial

      对于客户端而言,连接的建立会遇到如下几种情形:

  • 网络不可达或对方服务未启动

      如果传给Dial的Addr是可以立即判断出网络不可达,或者Addr中端口对应的服务没有启动,端口未被监听,Dial会几乎立即返回错误,比如:

 package main 

 import (
"net"
"log"
) func main() {
log.Println("begin dial...")
conn, err := net.Dial("tcp", ":8888")
if err != nil {
log.Println("dial error:", err)
return
}
defer conn.Close()
log.Println("dial ok")
}

网络不可达或对方服务未启动

      如果本机8888端口未有服务程序监听,那么执行上面程序,Dial会很快返回错误:

      注:在Centos6.5上测试,下同。

  • 对方服务的listen backlog满 

      还有一种场景就是对方服务器很忙,瞬间有大量client端连接尝试向server建立,server端的listen backlog队列满,server accept不及时((即便不accept,那么在backlog数量范畴里面,connect都会是成功的,因为new conn已经加入到server side的listen queue中了,accept只是从queue中取出一个conn而已),这将导致client端Dial阻塞。我们还是通过例子感受Dial的行为特点:

      服务端代码:

 package main 

 import (
"net"
"log"
"time"
) func main() {
l, err := net.Listen("tcp", ":8888")
if err != nil {
log.Println("error listen:", err)
return
}
defer l.Close()
log.Println("listen ok") var i int
for {
time.Sleep(time.Second * )
if _, err := l.Accept(); err != nil {
log.Println("accept error:", err)
break
}
i++
log.Printf("%d: accept a new connection\n", i)
}
}

server.go

      客户端代码:

 package main 

 import (
"net"
"log"
"time"
) func establishConn(i int) net.Conn {
conn, err := net.Dial("tcp", ":8888")
if err != nil {
log.Printf("%d: dial error: %s", i, err)
return nil
}
log.Println(i, ":connect to server ok")
return conn
} func main() {
var sl []net.Conn for i := ; i < ; i++ {
conn := establishConn(i)
if conn != nil {
sl = append(sl, conn)
}
} time.Sleep(time.Second * )
}

client.go

      经过测试在Client初始时成功地一次性建立了131个连接,然后后续每阻塞近1s才能成功建立一条连接。也就是说在server端 backlog满时(未及时accept),客户端将阻塞在Dial上,直到server端进行一次accept。

      如果server一直不accept,client端会一直阻塞么?我们去掉accept后的结果是:在Darwin下,client端会阻塞大 约1分多钟才会返回timeout。而如果server运行在ubuntu 14.04上,client似乎一直阻塞,我等了10多分钟依旧没有返回。 阻塞与否看来与server端的网络实现和设置有关。

      注:在Centos6.5上测试,发现注释掉server端的accept,client一次建立131个连接后,后面还会每隔1s建立一个链接。

  • 网络延迟较大,Dial阻塞并超时

      如果网络延迟较大,TCP握手过程将更加艰难坎坷(各种丢包),时间消耗的自然也会更长。Dial这时会阻塞,如果长时间依旧无法建立连接,则Dial也会返回“ getsockopt: operation timed out”错误。

      在连接建立阶段,多数情况下,Dial是可以满足需求的,即便阻塞一小会儿。但对于某些程序而言,需要有严格的连接时间限定,如果一定时间内没能成功建立连接,程序可能会需要执行一段“异常”处理逻辑,为此我们就需要DialTimeout了。下面的例子将Dial的最长阻塞时间限制在2s内,超出这个时长,Dial将返回timeout error:

 package main 

 import (
"net"
"log"
"time"
) func main() {
log.Println("begin dial...")
conn, err := net.DialTimeout("tcp", "192.168.30.134:8888", *time.Second)
if err != nil {
log.Println("dial error:", err)
return
}
defer conn.Close()
log.Println("dial ok")
}

client_timeout.go

      执行结果如下,需要模拟一个网络延迟大的环境:

 $go run client_timeout.go
// :: begin dial...
// :: dial error: dial tcp 104.236.176.96:: i/o timeout

(4)Socket读写

      连接建立起来后,我们就要在conn上进行读写,以完成业务逻辑。前面说过Go runtime隐藏了I/O多路复用的复杂性。语言使用者只需采用goroutine+Block I/O的模式即可满足大部分场景需求。Dial成功后,方法返回一个Conn接口类型变量值。

      客户端Dial建立连接:

func Dial(network, address string) (Conn, error)
 type Conn interface {
// Read reads data from the connection.
// Read can be made to time out and return an Error with Timeout() == true
// after a fixed time limit; see SetDeadline and SetReadDeadline.
Read(b []byte) (n int, err error) // Write writes data to the connection.
// Write can be made to time out and return an Error with Timeout() == true
// after a fixed time limit; see SetDeadline and SetWriteDeadline.
Write(b []byte) (n int, err error) // Close closes the connection.
// Any blocked Read or Write operations will be unblocked and return errors.
Close() error // LocalAddr returns the local network address.
LocalAddr() Addr // RemoteAddr returns the remote network address.
RemoteAddr() Addr // SetDeadline sets the read and write deadlines associated
// with the connection. It is equivalent to calling both
// SetReadDeadline and SetWriteDeadline.
//
// A deadline is an absolute time after which I/O operations
// fail with a timeout (see type Error) instead of
// blocking. The deadline applies to all future and pending
// I/O, not just the immediately following call to Read or
// Write. After a deadline has been exceeded, the connection
// can be refreshed by setting a deadline in the future.
//
// An idle timeout can be implemented by repeatedly extending
// the deadline after successful Read or Write calls.
//
// A zero value for t means I/O operations will not time out.
SetDeadline(t time.Time) error // SetReadDeadline sets the deadline for future Read calls
// and any currently-blocked Read call.
// A zero value for t means Read will not time out.
SetReadDeadline(t time.Time) error // SetWriteDeadline sets the deadline for future Write calls
// and any currently-blocked Write call.
// Even if write times out, it may return n > 0, indicating that
// some of the data was successfully written.
// A zero value for t means Write will not time out.
SetWriteDeadline(t time.Time) error
}

Conn接口

      服务器端Listen监听客户端连接:

func Listen(network, address string) (Listener, error)
 type Listener interface {
// Accept waits for and returns the next connection to the listener.
Accept() (Conn, error) // Close closes the listener.
// Any blocked Accept operations will be unblocked and return errors.
Close() error // Addr returns the listener's network address.
Addr() Addr
}

Listener 接口

      从Conn接口中有Read,Write,Close等方法。

 1)conn.Read的特点

  • Socket中无数据

      连接建立后,如果对方未发送数据到socket,接收方(Server)会阻塞在Read操作上,这和前面提到的“模型”原理是一致的。执行该Read操作的goroutine也会被挂起。runtime会监视该socket,直到其有数据才会重新调度该socket对应的Goroutine完成read。例子对应的代码文件:go-tcpsock/read_write下的client1.go和server1.go。

 package main

 import (
"log"
"net"
"time"
) func main() {
log.Println("begin dial...")
conn, err := net.Dial("tcp", ":8888")
if err != nil {
log.Println("dial error:", err)
return
}
defer conn.Close()
log.Println("dial ok")
time.Sleep(time.Second * )
}

client1.go

 //server.go

 package main

 import (
"log"
"net"
) func handleConn(c net.Conn) {
defer c.Close()
for {
// read from the connection
var buf = make([]byte, )
log.Println("start to read from conn")
n, err := c.Read(buf)
if err != nil {
log.Println("conn read error:", err)
return
}
log.Printf("read %d bytes, content is %s\n", n, string(buf[:n]))
}
} func main() {
l, err := net.Listen("tcp", ":8888")
if err != nil {
log.Println("listen error:", err)
return
} for {
c, err := l.Accept()
if err != nil {
log.Println("accept error:", err)
break
}
// start a new goroutine to handle
// the new connection.
log.Println("accept a new connection")
go handleConn(c)
}
}

server1.go

  • Socket中有部分数据

      如果socket中有部分数据,且长度小于一次Read操作所期望读出的数据长度,那么Read将会成功读出这部分数据并返回,而不是等待所有期望数据全部读取后再返回。

      客户端:

 //client2.go
package main import (
"fmt"
"log"
"net"
"os"
"time"
) func main() {
if len(os.Args) <= {
fmt.Println("usage: go run client2.go YOUR_CONTENT")
return
}
log.Println("begin dial...")
conn, err := net.Dial("tcp", ":8888")
if err != nil {
log.Println("dial error:", err)
return
}
defer conn.Close()
log.Println("dial ok") time.Sleep(time.Second * )
data := os.Args[]
conn.Write([]byte(data)) time.Sleep(time.Second * )
}

client2.go

      服务端:

 //server2.go
package main import (
"log"
"net"
) func handleConn(c net.Conn) {
defer c.Close()
for {
// read from the connection
var buf = make([]byte, )
log.Println("start to read from conn")
n, err := c.Read(buf)
if err != nil {
log.Println("conn read error:", err)
return
}
log.Printf("read %d bytes, content is %s\n", n, string(buf[:n]))
}
} func main() {
l, err := net.Listen("tcp", ":8888")
if err != nil {
log.Println("listen error:", err)
return
} for {
c, err := l.Accept()
if err != nil {
log.Println("accept error:", err)
break
}
// start a new goroutine to handle
// the new connection.
log.Println("accept a new connection")
go handleConn(c)
}
}

server2.go

      通过client2.go发送”hi”到Server端:

F:\Go\project\src\go_dev\go-tcpsock\read_write>go run client2.go hi
// :: begin dial...
// :: dial ok F:\Go\project\src\go_dev\go-tcpsock\read_write>go run server2.go
// :: accept a new connection
// :: start to read from conn
2019/03/04 22:43:43 read 2 bytes, content is hi
// :: start to read from conn
  • Socket中有足够数据

      如果socket中有数据,且长度大于等于一次Read操作所期望读出的数据长度,那么Read将会成功读出这部分数据并返回。这个情景是最符合我们对Read的期待的了:Read将用Socket中的数据将我们传入的slice填满后返回:n = 10, err = nil。

      执行结果:

F:\Go\project\src\go_dev\go-tcpsock\read_write>go run client2.go abcdefghij123
// :: begin dial...
// :: dial ok F:\Go\project\src\go_dev\go-tcpsock\read_write>go run server2.go
// :: accept a new connection
// :: start to read from conn
2019/03/04 22:50:03 read 10 bytes, content is abcdefghij
// :: start to read from conn
2019/03/04 22:50:03 read 3 bytes, content is 123
// :: start to read from conn

      结果分析: client端发送的内容长度为13个字节,Server端Read buffer的长度为10,因此Server Read第一次返回时只会读取10个字节;Socket中还剩余3个字节数据,Server再次Read时会把剩余数据读出(如:情形2)。

  • Socket关闭

      如果client端主动关闭了socket,那么Server的Read将会读到什么呢?

      这里分为“有数据关闭”和“无数据关闭”:

      有数据关闭是指在client关闭时,socket中还有server端未读取的数据。当client端close socket退出后,server依旧没有开始Read,10s后第一次Read成功读出了所有的数据,当第二次Read时,由于client端 socket关闭,Read返回EOF error。

      客户端:

 //client3.go
package main import (
"fmt"
"log"
"net"
"os"
"time"
) func main() {
if len(os.Args) <= {
fmt.Println("usage: go run client3.go YOUR_CONTENT")
return
}
log.Println("begin dial...")
conn, err := net.Dial("tcp", ":8888")
if err != nil {
log.Println("dial error:", err)
return
}
defer conn.Close()
log.Println("dial ok") time.Sleep(time.Second * )
data := os.Args[]
conn.Write([]byte(data))
}

client3.go

服务端:

 //server3.go

 package main

 import (
"log"
"net"
"time"
) func handleConn(c net.Conn) {
defer c.Close()
for {
// read from the connection
time.Sleep( * time.Second)
var buf = make([]byte, )
log.Println("start to read from conn")
n, err := c.Read(buf)
if err != nil {
log.Println("conn read error:", err)
return
}
log.Printf("read %d bytes, content is %s\n", n, string(buf[:n]))
}
} func main() {
l, err := net.Listen("tcp", ":8888")
if err != nil {
log.Println("listen error:", err)
return
} for {
c, err := l.Accept()
if err != nil {
log.Println("accept error:", err)
break
}
// start a new goroutine to handle
// the new connection.
log.Println("accept a new connection")
go handleConn(c)
}
}

server3.go

      执行结果:

F:\Go\project\src\go_dev\go-tcpsock\read_write>go run client3.go hello
// :: begin dial...
// :: dial ok F:\Go\project\src\go_dev\go-tcpsock\read_write>go run server3.go
// :: accept a new connection
// :: start to read from conn
// :: read bytes, content is hello
// :: start to read from conn
// :: conn read error: EOF

      结果分析:从输出结果来看,当client端close socket退出后,server3依旧没有开始Read,10s后第一次Read成功读出了5个字节的数据,当第二次Read时,由于client端 socket关闭,Read返回EOF error。

      通过上面这个例子,我们也可以猜测出“无数据关闭”情形下的结果,那就是Read直接返回EOF error。

  • 读取操作超时

      有些场合对Read的阻塞时间有严格限制,在这种情况下,Read的行为到底是什么样的呢?在返回超时错误时,是否也同时Read了一部分数据了呢? 这个实验比较难于模拟,下面的测试结果也未必能反映出所有可能结果。

      客户端:

 //client4.go
package main import (
"log"
"net"
"time"
) func main() {
log.Println("begin dial...")
conn, err := net.Dial("tcp", ":8888")
if err != nil {
log.Println("dial error:", err)
return
}
defer conn.Close()
log.Println("dial ok") data := make([]byte, )
conn.Write(data) time.Sleep(time.Second * )
}

client4.go

      服务端:

 //server4.go

 package main

 import (
"log"
"net"
"time"
) func handleConn(c net.Conn) {
defer c.Close()
for {
// read from the connection
time.Sleep( * time.Second)
var buf = make([]byte, )
log.Println("start to read from conn")
//c.SetReadDeadline(time.Now().Add(time.Microsecond * 10))//conn read 0 bytes, error: read tcp 127.0.0.1:8888->127.0.0.1:60763: i/o timeout
c.SetReadDeadline(time.Now().Add(time.Microsecond * ))
n, err := c.Read(buf)
if err != nil {
log.Printf("conn read %d bytes, error: %s", n, err)
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
continue
}
return
} log.Printf("read %d bytes, content is %s\n", n, string(buf[:n]))
}
} func main() {
l, err := net.Listen("tcp", ":8888")
if err != nil {
log.Println("listen error:", err)
return
} for {
c, err := l.Accept()
if err != nil {
log.Println("accept error:", err)
break
}
// start a new goroutine to handle
// the new connection.
log.Println("accept a new connection")
go handleConn(c)
}
}

server4.go

      在Server端我们通过Conn的SetReadDeadline方法设置了10微秒的读超时时间。

      虽然每次都是10微秒超时,但结果不同,第一次Read超时,读出数据长度为0;第二次读取所有数据成功,没有超时。反复执行了多次,没能出现“读出部分数据且返回超时错误”的情况。

 2)conn.Write的特点

  • 成功写

      前面例子着重于Read,client端在Write时并未判断Write的返回值。所谓“成功写”指的就是Write调用返回的n与预期要写入的数据长度相等,且error = nil。这是我们在调用Write时遇到的最常见的情形,这里不再举例了。

  • 写阻塞

      TCP连接通信两端的OS都会为该连接保留数据缓冲,一端调用Write后,实际上数据是写入到OS的协议栈的数据缓冲的。TCP是全双工通信,因此每个方向都有独立的数据缓冲。当发送方将对方的接收缓冲区以及自身的发送缓冲区写满后,Write就会阻塞。

      客户端:

 //client5.go
package main import (
"log"
"net"
"time"
) func main() {
log.Println("begin dial...")
conn, err := net.Dial("tcp", ":8888")
if err != nil {
log.Println("dial error:", err)
return
}
defer conn.Close()
log.Println("dial ok") data := make([]byte, )
var total int
for {
n, err := conn.Write(data)
if err != nil {
total += n
log.Printf("write %d bytes, error:%s\n", n, err)
break
}
total += n
log.Printf("write %d bytes this time, %d bytes in total\n", n, total)
} log.Printf("write %d bytes in total\n", total)
time.Sleep(time.Second * )
}

client5.go

      服务端:

 //server5.go

 package main

 import (
"log"
"net"
"time"
) func handleConn(c net.Conn) {
defer c.Close()
time.Sleep(time.Second * )
for {
// read from the connection
time.Sleep( * time.Second)
var buf = make([]byte, )
log.Println("start to read from conn")
n, err := c.Read(buf)
if err != nil {
log.Printf("conn read %d bytes, error: %s", n, err)
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
continue
}
break
} log.Printf("read %d bytes, content is %s\n", n, string(buf[:n]))
}
} func main() {
l, err := net.Listen("tcp", ":8888")
if err != nil {
log.Println("listen error:", err)
return
} for {
c, err := l.Accept()
if err != nil {
log.Println("accept error:", err)
break
}
// start a new goroutine to handle
// the new connection.
log.Println("accept a new connection")
go handleConn(c)
}
}

server5.go

      执行结果:

[root@centos tcp]# go run client5.go
// :: begin dial...
// :: dial ok
// :: write bytes this time, bytes in total
// :: write bytes this time, bytes in total
// :: write bytes this time, bytes in total
// :: write bytes this time, bytes in total
// :: write bytes this time, bytes in total
// :: write bytes this time, bytes in total // :: write bytes this time, bytes in total
// :: write bytes this time, bytes in total [root@centos tcp]# go run server5.go
// :: accept a new connection
// :: start to read from conn
// :: read bytes, content is
// :: start to read from conn
// :: read bytes, content is
// :: start to read from conn
// :: read bytes, content is

      Server5在前10s中并不Read数据,因此当client5一直尝试写入时,写到一定量后就会发生阻塞。

      在Centos6.5上测试,这个size大约在 393216 bytes。后续当server5每隔5s进行Read时,OS socket缓冲区腾出了空间,client5就又可以写入。

  • 写入部分数据

      Write操作存在写入部分数据的情况,比如上面例子中,当client端输出日志停留在“2019/03/04 23:30:39 write 65536 bytes this time, 524288 bytes in total”时,我们杀掉server5,这时我们会看到client5输出以下日志:

[root@centos tcp]# go run client5.go
// :: begin dial...
// :: dial ok
// :: write bytes this time, bytes in total
// :: write bytes this time, bytes in total
// :: write bytes this time, bytes in total
// :: write bytes this time, bytes in total
// :: write bytes this time, bytes in total
// :: write bytes this time, bytes in total // :: write bytes this time, bytes in total
// :: write bytes this time, bytes in total
// :: write bytes, error:write tcp 127.0.0.1:->127.0.0.1:: write: connection reset by peer
2019/03/04 23:30:45 write 573440 bytes in total

      显然Write并非在 524288 bytes 这个地方阻塞的,而是后续又写入49152 bytes 后发生了阻塞,server端socket关闭后,我们看到Wrote返回er != nil且n = 49152,程序需要对这部分写入的49152 字节做特定处理。

  •  写入超时

      如果非要给Write增加一个期限,那我们可以调用SetWriteDeadline方法。我们copy一份client5.go,形成client6.go,在client6.go的Write之前增加一行timeout设置代码:

conn.SetWriteDeadline(time.Now().Add(time.Microsecond * ))
 //client6.go
package main import (
"log"
"net"
"time"
) func main() {
log.Println("begin dial...")
conn, err := net.Dial("tcp", ":8888")
if err != nil {
log.Println("dial error:", err)
return
}
defer conn.Close()
log.Println("dial ok") data := make([]byte, )
var total int
for {
conn.SetWriteDeadline(time.Now().Add(time.Microsecond * ))
n, err := conn.Write(data)
if err != nil {
total += n
log.Printf("write %d bytes, error:%s\n", n, err)
break
}
total += n
log.Printf("write %d bytes this time, %d bytes in total\n", n, total)
} log.Printf("write %d bytes in total\n", total)
time.Sleep(time.Second * )
}

client6.go

启动server6.go,启动client6.go,我们可以看到写入超时的情况下,Write的返回结果:

[root@centos tcp]# go run client6.go
// :: begin dial...
// :: dial ok
// :: write bytes this time, bytes in total
// :: write bytes this time, bytes in total
// :: write bytes, error:write tcp 127.0.0.1:->127.0.0.1:: i/o timeout
// :: write bytes in total

可以看到在写入超时时,依旧存在部分数据写入的情况。

综上例子,虽然Go给我们提供了阻塞I/O的便利,但在调用Read和Write时依旧要综合需要方法返回的n和err的结果,以做出正确处理。net.conn实现了io.Reader和io.Writer接口,因此可以试用一些wrapper包进行socket读写,比如bufio包下面的Writer和Reader、io/ioutil下的函数等。

(5)Goroutine safe

基于goroutine的网络架构模型,存在在不同goroutine间共享conn的情况,那么conn的读写是否是goroutine safe的呢?在深入这个问题之前,我们先从应用意义上来看read操作和write操作的goroutine-safe必要性。
      对于read操作而言,由于TCP是面向字节流,conn.Read无法正确区分数据的业务边界,因此多个goroutine对同一个conn进行read的意义不大,goroutine读到不完整的业务包反倒是增加了业务处理的难度。对与Write操作而言,倒是有多个goroutine并发写的情况。不过conn读写是否goroutine-safe的测试不是很好做,我们先深入一下runtime代码,先从理论上给这个问题定个性:

net.conn只是*netFD的wrapper结构,最终Write和Read都会落在其中的fd上:

type conn struct {
fd *netFD
}

netFD在不同平台上有着不同的实现,我们以net/fd_unix.go中的netFD为例:

// Network file descriptor.
type netFD struct {
// locking/lifetime of sysfd + serialize access to Read and Write methods
fdmu fdMutex // immutable until Close
sysfd int
family int
sotype int
isConnected bool
net string
laddr Addr
raddr Addr // wait server
pd pollDesc
}

我们看到netFD中包含了一个runtime实现的fdMutex类型字段,从注释上来看,该fdMutex用来串行化对该netFD对应的sysfd的Write和Read操作。从这个注释上来看,所有对conn的Read和Write操作都是有fdMutex互斥的,从netFD的Read和Write方法的实现也证实了这一点:

 func (fd *netFD) Read(p []byte) (n int, err error) {
if err := fd.readLock(); err != nil {
return , err
}
defer fd.readUnlock()
if err := fd.pd.PrepareRead(); err != nil {
return , err
}
for {
n, err = syscall.Read(fd.sysfd, p)
if err != nil {
n =
if err == syscall.EAGAIN {
if err = fd.pd.WaitRead(); err == nil {
continue
}
}
}
err = fd.eofError(n, err)
break
}
if _, ok := err.(syscall.Errno); ok {
err = os.NewSyscallError("read", err)
}
return
} func (fd *netFD) Write(p []byte) (nn int, err error) {
if err := fd.writeLock(); err != nil {
return , err
}
defer fd.writeUnlock()
if err := fd.pd.PrepareWrite(); err != nil {
return , err
}
for {
var n int
n, err = syscall.Write(fd.sysfd, p[nn:])
if n > {
nn += n
}
if nn == len(p) {
break
}
if err == syscall.EAGAIN {
if err = fd.pd.WaitWrite(); err == nil {
continue
}
}
if err != nil {
break
}
if n == {
err = io.ErrUnexpectedEOF
break
}
}
if _, ok := err.(syscall.Errno); ok {
err = os.NewSyscallError("write", err)
}
return nn, err
}

Read Write

每次Write操作都是受lock保护,直到此次数据全部write完。因此在应用层面,要想保证多个goroutine在一个conn上write操作的Safe,需要一次write完整写入一个“业务包”;一旦将业务包的写入拆分为多次write,那就无法保证某个Goroutine的某“业务包”数据在conn发送的连续性。

同时也可以看出即便是Read操作,也是lock保护的。多个Goroutine对同一conn的并发读不会出现读出内容重叠的情况,但内容断点是依 runtime调度来随机确定的。存在一个业务包数据,1/3内容被goroutine-1读走,另外2/3被另外一个goroutine-2读 走的情况。比如一个完整包:world,当goroutine的read slice size < 5时,存在可能:一个goroutine读到 “worl”,另外一个goroutine读出”d”。

 (6)Socket属性

原生Socket API提供了丰富的sockopt设置接口,但Golang有自己的网络架构模型,golang提供的socket options接口也是基于上述模型的必要的属性设置。包括

SetKeepAlive
SetKeepAlivePeriod
SetLinger
SetNoDelay (默认no delay)
SetWriteBuffer
SetReadBuffer

不过上面的Method是TCPConn的,而不是Conn的,要使用上面的Method的,需要type assertion:

tcpConn, ok := c.(*TCPConn)
if !ok {
//error handle
} tcpConn.SetNoDelay(true)

对于listener socket, golang默认采用了 SO_REUSEADDR,这样当你重启 listener程序时,不会因为address in use的错误而启动失败。而listen backlog的默认值是通过获取系统的设置值得到的。不同系统不同:mac 128, linux 512等。

 (7)关闭连接

和前面的方法相比,关闭连接算是最简单的操作了。由于socket是全双工的,client和server端在己方已关闭的socket和对方关闭的socket上操作的结果有不同。看下面例子:

客户端:

 package main

 import (
"log"
"net"
"time"
) func main() {
log.Println("begin dial...")
conn, err := net.Dial("tcp", ":8888")
if err != nil {
log.Println("dial error:", err)
return
}
conn.Close()
log.Println("close ok") var buf = make([]byte, )
n, err := conn.Read(buf)
if err != nil {
log.Println("read error:", err)
} else {
log.Printf("read % bytes, content is %s\n", n, string(buf[:n]))
} n, err = conn.Write(buf)
if err != nil {
log.Println("write error:", err)
} else {
log.Printf("write % bytes, content is %s\n", n, string(buf[:n]))
} time.Sleep(time.Second * )
}

client.go

服务端:

 //server.go

 package main

 import (
"log"
"net"
) func handleConn(c net.Conn) {
defer c.Close() // read from the connection
var buf = make([]byte, )
log.Println("start to read from conn")
n, err := c.Read(buf)
if err != nil {
log.Println("conn read error:", err)
} else {
log.Printf("read %d bytes, content is %s\n", n, string(buf[:n]))
} n, err = c.Write(buf)
if err != nil {
log.Println("conn write error:", err)
} else {
log.Printf("write %d bytes, content is %s\n", n, string(buf[:n]))
}
} func main() {
l, err := net.Listen("tcp", ":8888")
if err != nil {
log.Println("listen error:", err)
return
} for {
c, err := l.Accept()
if err != nil {
log.Println("accept error:", err)
break
}
// start a new goroutine to handle
// the new connection.
log.Println("accept a new connection")
go handleConn(c)
}
}

server.go

上述例子的执行结果如下:

[root@centos conn_close]# go run client1.go
// :: begin dial...
// :: close ok
// :: read error: read tcp 127.0.0.1:->127.0.0.1:: use of closed network connection
// :: write error: write tcp 127.0.0.1:->127.0.0.1:: use of closed network connection [root@centos conn_close]# go run server1.go
// :: accept a new connection
// :: start to read from conn
// :: conn read error: EOF
// :: write bytes, content is

从client的结果来看,在己方已经关闭的socket上再进行read和write操作,会得到”use of closed network connection” error;
      从server的执行结果来看,在对方关闭的socket上执行read操作会得到EOF error,但write操作会成功,因为数据会成功写入己方的内核socket缓冲区中,即便最终发不到对方socket缓冲区了,因为己方socket并未关闭。因此当发现对方socket关闭后,己方应该正确合理处理自己的socket,再继续write已经无任何意义了。

(8)发送http请求

Get请求协议的格式如下:
请求首行; // 请求方式 请求路径 协议和版本,例如:GET /index.html HTTP/1.1
请求头信息;// 请求头名称:请求头内容,即为key:value格式,例如:Host:localhost
空行; // 用来与请求体分隔开
请求体。 // GET没有请求体,只有POST有请求体。 例如:
GET /books/?sex=man&name=Professional HTTP/1.1
Host: www.wrox.com
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.)
Gecko/ Firefox/1.0.
Connection: Keep-Alive

关于Http协议可以看下博客:http://www.cnblogs.com/yuanchenqi/articles/6000358.html

Get和Post请求的区别可以看下博客:https://www.cnblogs.com/logsharing/p/8448446.html

依照上面Get的请求协议格式,我们给百度发一个Get请求:

 package main

 import (
"fmt"
"io"
"net"
)
func main() { conn, err := net.Dial("tcp", "www.baidu.com:80")
if err != nil {
fmt.Println("Error dialing", err.Error())
return
}
defer conn.Close() msg := "GET / HTTP/1.1\r\n"
msg += "Host: www.baidu.com\r\n"
msg += "Connection: close\r\n"
msg += "\r\n\r\n" _, err = io.WriteString(conn, msg)
if err != nil {
fmt.Println("write string failed, ", err)
return
}
buf := make([]byte, )
for {
count, err := conn.Read(buf)
if err != nil {
break
}
fmt.Println(string(buf[:count]))
}
}

Get 访问百度

(9)小结

本文比较基础,但却很重要,毕竟golang是面向大规模服务后端的,对通信环节的细节的深入理解会大有裨益。另外Go的goroutine+阻塞通信的网络通信模型降低了开发者心智负担,简化了通信的复杂性,这点尤为重要。

 注:上面例子出现(root@centos)表示是在Centos6.5上运行,其他是在Windows上运行,go version go1.8 windows/amd64。

特别注意:

  • 上面内容除一小部分(运行结果及其他博客链接部分)全部来自 https://tonybai.com/2015/11/17/tcp-programming-in-golang/ 该博主,解释权归该博主。
  • 本节用到的例子在该博主github地址:https://github.com/bigwhite/experiments/tree/master/go-tcpsock

2. Redis使用

  (1)Redis简介

  • redis是个开源的高性能的key-value的内存数据库,可以把它当成远程的数据结构。
  • 支持的value类型非常多,比如string、list(链表)、set(集合)、hash表等等。
  • redis性能非常高,单机能够达到15w qps,通常适合做缓存。

  (2)下载并安装依赖

使用第三方开源的redis库: github.com/garyburd/redigo/redis
go get github.com/garyburd/redigo/redis

   注意:go get 从指定源上面下载或者更新指定的代码和依赖,并对他们进行编译和安装(相当于 clone + install)。

               更多命令使用可以看:https://www.flysnow.org/2017/03/08/go-in-action-go-tools.html

  (3)操作Redis

  • 连接redis
 package main

 import (
"fmt"
"github.com/garyburd/redigo/redis"
) func main() {
fmt.Println("start to connect redis...")
c, err := redis.Dial("tcp", "192.168.30.134:6379")
if err != nil {
fmt.Println("conn redis failed,", err)
return
} defer c.Close()
}

connect redis

  • 字符串 Set 操作
 package main

 import (
"fmt"
"github.com/garyburd/redigo/redis"
) func main() {
c, err := redis.Dial("tcp", "192.168.30.134:6379")
if err != nil {
fmt.Println("conn redis failed,", err)
return
} defer c.Close()
_, err = c.Do("Set", "abc", )
if err != nil {
fmt.Println(err)
return
} r, err := redis.Int(c.Do("Get", "abc"))
if err != nil {
fmt.Println("get abc failed,", err)
return
} fmt.Println(r)
}

String Set

  • Hash表
 package main

 import (
"fmt"
"github.com/garyburd/redigo/redis"
) func main() {
c, err := redis.Dial("tcp", "192.168.30.134:6379")
if err != nil {
fmt.Println("conn redis failed,", err)
return
} defer c.Close()
_, err = c.Do("HSet", "books", "abc", )
if err != nil {
fmt.Println(err)
return
} r, err := redis.Int(c.Do("HGet", "books", "abc"))
if err != nil {
fmt.Println("get abc failed,", err)
return
} fmt.Println(r)
}

Hash

  • 批量Set
 package main

 import (
"fmt"
"github.com/garyburd/redigo/redis"
) func main() {
c, err := redis.Dial("tcp", "192.168.30.134:6379")
if err != nil {
fmt.Println("conn redis failed,", err)
return
} defer c.Close()
_, err = c.Do("MSet", "abc", , "efg", )
if err != nil {
fmt.Println(err)
return
} r, err := redis.Ints(c.Do("MGet", "abc", "efg"))
if err != nil {
fmt.Println("get abc failed,", err)
return
} for _, v := range r {
fmt.Println(v)
}
}

batch set

  • 过期时间
 package main

 import (
"fmt"
"time"
"github.com/garyburd/redigo/redis"
) func main() {
c, err := redis.Dial("tcp", "192.168.30.134:6379")
if err != nil {
fmt.Println("conn redis failed,", err)
return
}
defer c.Close() _, err = c.Do("Set", "abc", )
if err != nil {
fmt.Println(err)
return
} r, err := redis.Int(c.Do("Get", "abc"))
if err != nil {
fmt.Println("get abc failed,", err)
return
}
fmt.Println("abc = ", r) _, err = c.Do("expire", "abc", ) //5s后过期
if err != nil {
fmt.Println(err)
return
} time.Sleep(*time.Second) r, err = redis.Int(c.Do("Get", "abc"))
if err != nil {
fmt.Println("get abc failed,", err)
return
}
fmt.Println("abc = ", r) //get abc failed, redigo: nil returned
}

expire

  • 队列操作
 package main

 import (
"fmt"
"github.com/garyburd/redigo/redis"
) func main() {
c, err := redis.Dial("tcp", "192.168.30.134:6379")
if err != nil {
fmt.Println("conn redis failed,", err)
return
} defer c.Close()
_, err = c.Do("lpush", "book_list", "abc", "ceg", )
if err != nil {
fmt.Println(err)
return
} r, err := redis.String(c.Do("lpop", "book_list"))
if err != nil {
fmt.Println("get abc failed,", err)
return
} fmt.Println(r)
}

push

上面只列出了redis几个基本操作,Redis更加详细操作可以看我的这篇博客(用Python API): https://www.cnblogs.com/xuejiale/p/10460468.html

  • Redis连接池

   先看实现连接池的例子:

在cinfig.go中主要是连接池一些参数的设置,在pool.go中实现获取连接池接口,在main.go中是调连接池的接口应用。

 package redisConf

 var RedisConf = map[string]string{
"name": "redis",
"type": "tcp",
"address": "192.168.30.134:6379",
"auth": "*****", //如果有密码,写成自己的密码
}

conf.go

 package redisPool

 import (
"go_dev/day9/go_redis/redis_poll/redisConf" //改成你自己的包目录
"github.com/garyburd/redigo/redis"
"time"
) var RedisClient *redis.Pool func init() {
// 建立连接池
RedisClient = &redis.Pool {
MaxIdle: ,
MaxActive: ,
IdleTimeout: * time.Second,
Dial: func() (redis.Conn, error) {
c, err := redis.Dial(redisConf.RedisConf["type"], redisConf.RedisConf["address"])
if err != nil {
return nil, err
}
//如果redis设置了密码,需要下面的验证
// if _, err := c.Do("AUTH", redisConf.RedisConf["auth"]); err != nil {
// c.Close()
// return nil, err
// }
return c, nil
},
//每次获取连接前做一次check
TestOnBorrow: func(c redis.Conn, t time.Time) error {
if time.Since(t) < time.Minute {
return nil
}
_, err := c.Do("PING")
return err
},
}
}

pool.go

 package main

 import (
"go_dev/day9/go_redis/redis_poll/redisPool" //改成你自己的包目录
"fmt"
"github.com/garyburd/redigo/redis"
) var RedisExpire = //缓存有效期 func main() { // 从池里获取连接
rc := redisPool.RedisClient.Get()
// 用完后将连接放回连接池
defer rc.Close() key := "redis.cache"
//设置值
_, err := rc.Do("Set", key, "", "EX", RedisExpire)
if err != nil {
fmt.Println(err)
return
}
//取出值
val, err := redis.String(rc.Do("Get", key))
if err != nil {
fmt.Println(err)
}
fmt.Println(val)
//删除
_, err = rc.Do("Del", key)
if err != nil {
fmt.Println(err)
return
}
}

main.go

 func initRedis() {
// 建立连接池
pool := &redis.Pool {
MaxIdle: ,
MaxActive: ,
IdleTimeout: * time.Second,
Dial: func() (redis.Conn, error) {
return redis.Dial("tcp", "localhost:6379")
},
}
}

简写版 连接池

目录结构如下:

分析:首先来看Pool这个结构体及结构体中各个变量的含义:

 type Pool struct {
// Dial is an application supplied function for creating and configuring a
// connection.
//
// The connection returned from Dial must not be in a special state
// (subscribed to pubsub channel, transaction started, ...).
Dial func() (Conn, error) // TestOnBorrow is an optional application supplied function for checking
// the health of an idle connection before the connection is used again by
// the application. Argument t is the time that the connection was returned
// to the pool. If the function returns an error, then the connection is
// closed.
TestOnBorrow func(c Conn, t time.Time) error // Maximum number of idle connections in the pool.
MaxIdle int // Maximum number of connections allocated by the pool at a given time.
// When zero, there is no limit on the number of connections in the pool.
MaxActive int // Close connections after remaining idle for this duration. If the value
// is zero, then idle connections are not closed. Applications should set
// the timeout to a value less than the server's timeout.
IdleTimeout time.Duration // If Wait is true and the pool is at the MaxActive limit, then Get() waits
// for a connection to be returned to the pool before returning.
Wait bool // Close connections older than this duration. If the value is zero, then
// the pool does not close connections based on age.
MaxConnLifetime time.Duration chInitialized uint32 // set to 1 when field ch is initialized mu sync.Mutex // mu protects the following fields
closed bool // set to true when the pool is closed.
active int // the number of open connections in the pool
ch chan struct{} // limits open connections when p.Wait is true
idle idleList // idle connections
}

Pool struct

主要看这几个参数:

Dial:是必须要实现的,就是调用普通的的redis.Dial即可。
MaxIdle:最大的空闲连接数,表示即使没有redis连接时依然可以保持N个空闲的连接,而不被清除,随时处于待命状态。
MaxActive:最大的激活连接数,表示同时最多有N个连接,也就是并发数。
IdleTimeout:最大的空闲连接等待时间,超过此时间后,空闲连接将被关闭。
Wait:当连接数已满,是否要阻塞等待获取连接。false表示不等待,直接返回错误。
TestOnBorrow:在获取conn的时候会调用一次这个方法,来保证连接可用(其实也不是一定可用,因为test成功以后依然有可能被干掉),这个方法是可选项,一般这个方法是去调用
一个redis的ping方法,看项目需求了,如果并发很高,想极限提高速度,这个可以不设置。如果想增加点连接可用性,还是加上比较好。

Pool中的方法及具体实现可以看下面的链接:

https://github.com/garyburd/redigo/blob/master/redis/pool.go#L122
https://studygolang.com/articles/9642 (连接池代码分析)
https://blog.csdn.net/xiaohu50/article/details/51606349 (redis.Pool 配置参数调优)
(重点)GO操作redis更多API可以看: https://godoc.org/github.com/garyburd/redigo/redis#Ints,结合上面的操作就可以熟练操作Redis
  • 管道操作

请求/响应服务可以实现持续处理新请求,即使客户端没有准备好读取旧响应。这样客户端可以发送多个命令到服务器而无需等待响应,最后在一次读取多个响应。这就是管道化(pipelining),这个技术在多年就被广泛使用了。距离,很多POP3协议实现已经支持此特性,显著加速了从服务器下载新邮件的过程。Redis很早就支持管道化,所以无论你使用任何版本,你都可以使用管道化技术
   连接支持使用Send(),Flush(),Receive()方法支持管道化操作。

Send(commandName string, args ...interface{}) error
Flush() error
Receive() (reply interface{}, err error)

Send向连接的输出缓冲中写入命令。
   Flush将连接的输出缓冲清空并写入服务器端。
   Recevie按照FIFO顺序依次读取服务器的响应。

c.Send("SET", "foo", "bar")
c.Send("GET", "foo")
c.Flush()
c.Receive() // reply from SET
v, err = c.Receive() // reply from GET

上面如果再一次 c.Receive() 则会 hang 住,因为只发送了两条命令,执行结果也就只有两条,再去取管道中无输出,因此会hang住。

 package main

 import (
"fmt"
"github.com/garyburd/redigo/redis"
) func main() {
c, err := redis.Dial("tcp", "192.168.30.134:6379")
if err != nil {
fmt.Println("conn redis failed, err:", err)
return
}
defer c.Close() c.Send("SET", "foo", "bar")
c.Send("GET", "foo") c.Flush() v, err := c.Receive()
fmt.Printf("v:%v,err:%v\n", v, err) // v:OK,err:<nil>
v, err = c.Receive()
fmt.Printf("v:%s,err:%v\n", v, err) // v:bar,err:<nil>
//fmt.Printf("v:%v,err:%v\n", v, err) //v:[98 97 114],err:<nil> v, err = c.Receive() // hang住,一直等待
fmt.Printf("v:%v,err:%v\n", v, err)
}

hang example

Do方法结合了Send, Flush and Receive方法的功能。开始Do方法往管道写入命令并且刷新输出缓冲。接下来Do方法会接收所有就绪的回复包括最近发送的命令。如果收到的回复中有错误,则Do就会返回错误。如果没有错误,Do会返回最近一次收到的回复。如果发送的命令是空(""),则Do方法会刷新输出缓冲收到就绪的回复而不用发送命令。

使用发送和Do方法可以实现一个管道事务:

c.Send("MULTI")
c.Send("INCR", "foo")
c.Send("INCR", "bar")
r, err := c.Do("EXEC")
fmt.Println(r) // prints [1, 1]

一个连接支持并发的Receive和并发的Send,Flush,但不支持并发的Do方法。

  • 发布订阅

使用Send, Flush 和 Receive可以实现发布订阅的订阅者:

c.Send("SUBSCRIBE", "example")
c.Flush()
for {
reply, err := c.Receive()
if err != nil {
return err
}
// process pushed message
}

这 PubSubConn 类型封装了连接(Conn)很方便的实现订阅者。订阅,发布订阅,不订阅,发布不订阅方法发送并且刷新订阅管理命令。Receive方法将推送的消息转换为特定的类型。

 psc := redis.PubSubConn{Conn: c}
psc.Subscribe("example")
for {
switch v := psc.Receive().(type) {
case redis.Message: //单个订阅subscribe
fmt.Printf("%s: message: %s\n", v.Channel, v.Data)
case redis.Subscription: //模式订阅psubscribe
fmt.Printf("%s: %s %d\n", v.Channel, v.Kind, v.Count)
case error:
return v
}
}

发布订阅

图书管理系统v3:

使用redis存储数据完善之前的图书管理系统?

 参考文献:

  • https://tonybai.com/2015/11/17/tcp-programming-in-golang/
  • https://godoc.org/github.com/garyburd/redigo/redis#Ints
  • https://blog.csdn.net/guyan0319/article/details/84944059 (Go Redis连接池)
  • https://www.jianshu.com/p/2d3db51d5bbe
  • https://studygolang.com/articles/12230 (golang redis连接池的使用)
  • https://www.cnblogs.com/suoning/p/7259106.html

Go语言学习之9 网络协议TCP、Redis与聊天室的更多相关文章

  1. 网络协议TCP、Http、webservice、socket区别

    网络协议TCP.Http.webservice.socket区别 http 和 webservice 都是基于TCP/IP协议的应用层协议 webservice是基于http的soap协议传输数据 w ...

  2. 网络协议TCP/IP、IPX/SPX、NETBEUI简介

    网络中不同的工作站,服务器之间能传输数据,源于协议的存在.随着网络的发展,不同的开发商开发了不同的通信方式.为了使通信成功可靠,网络中的所有主机都必须使用同一语言,不能带有方言.因而必须开发严格的标准 ...

  3. python网络编程--socket,网络协议,TCP

    一. 客户端/服务端架构(用到网络通信的地方) 我们使用qq.微信和别人聊天,通过浏览器来浏览页面.看京东的网站,通过优酷.快播(此处只是怀念一下)看片片啥的等等,通过无线打印机来打印一个word文档 ...

  4. 网络体系结构的概念 - 网络协议TCP - 红黑联盟

    https://i.cnblogs.com/EditPosts.aspx?opt=1 网络体系结构的概念  计算机网络就是一组通过一定形式连接起来的计算机系统,它需要四个要素的支持,即通信线路和通信设 ...

  5. 学习JavaSE TCP/IP协议与搭建简易聊天室

    一.TCP/IP协议 1.TCP/IP协议包括TCP.IP和UDP等 2.域名通过dns服务器转换为IP地址 3.局域网可以通过IP或者主机地址寻找到相应的主机 4.TCP是可靠的连接,效率低,且连接 ...

  6. 网络协议之:redis protocol 详解

    目录 简介 redis的高级用法 Redis中的pipline Redis中的Pub/Sub RESP protocol Simple Strings Bulk Strings RESP Intege ...

  7. 网络基础编程_5.4聊天室-IOCP服务器

    聊天室-IOCP服务器 main 创建完成端口内核对象(CreateIoCompletionPort) 获取核心数并创建线程(GetSystemInfo + CreateThread) 创建套接字并绑 ...

  8. [网络协议]TCP粘包分析

    关于socket粘包,socket缓冲区设置的问题,记录一下: 一 .两个简单概念长连接与短连接: 长连接     Client方与Server方先建立通讯连接,连接建立后不断开, 然后再进行报文发送 ...

  9. 网络协议TCP

    TCP:传输控制协议 tcp的特点:面向连接(打电话模型),可靠传输 tcp通信的三个步骤: 1.通信双方建立连接 2.收发收据 3.关闭连接 tcp客户端实现流程 """ ...

随机推荐

  1. Java Web 笔试(面试)题

    1.Servlet 的生命周期,并说出 Servlet 与 CGI 的区别 Web 容器加载 Servlet 并将其实例化后,Servlet 生命周期开始,容器运行其 init 方法进行 Servle ...

  2. bootstrap-treeview 中文开发手册

    官方文档URL:  https://www.npmjs.com/package/bootstrap-treeview 2017年11月21日10:45:10 演示:http://www.htmleaf ...

  3. redash学习记录

    一.简介 一款开源的 BI 工具Redash 二.参考资料 一款开源的 BI 工具Redash 浅析数据查询与可视化工具--Redash

  4. nginx配置框架问题

    1.框架源文件没有引入 2.nginx fastcgi.conf配置允许访问上级目录地址 3.使用autoindex on;参数

  5. 016-并发编程-java.util.concurrent.locks之-Lock及ReentrantLock

    一.概述 重入锁ReentrantLock,就是支持重进入的锁 ,它表示该锁能够支持一个线程对资源的重复加锁.支持公平性与非公平性选择,默认为非公平. 以下梳理ReentrantLock.作为依赖于A ...

  6. Redis入门到高可用(二十)——Redis Cluster

    一.呼唤集群 二.数据分布概论      三.哈希分布 1.节点取余 2.一致性哈希 添加一个node5节点时,只影响n1和n2之间的数据   3.虚拟槽分区 四.基本架构 五.redis clust ...

  7. Ceph与Gluster之开源存储的对比

    一.Ceph与Gluster之开源存储的对比 一.Ceph与Gluster的原理对比 Ceph和Gluster是Red Hat旗下的成熟的开源存储产品,Ceph与Gluster在原理上有着本质上的不同 ...

  8. 希尔排序(Python实现)

    目录 1.for版本--希尔排序 2. while版本--希尔排序 3. 测试用例 4. 算法时间复杂度分析 1.for版本--希尔排序 def shell_sort_for(a_list): ''' ...

  9. 国内老版本ubuntu更新源地址以及sources.list的配置方法

    在终端输入并运行 sudo apt-get install vimsudo cp /etc/apt/sources.list /etc/apt/sources.list.backup (备份当前的源列 ...

  10. https://scrapingclub.com/exercise/basic_captcha/

    def parse(self, response): # set_cookies = response.headers.getlist("set-cookie").decode(& ...