├── .gitignore ├── go.mod ├── clientaddr.go ├── mockconn.go ├── go.sum ├── README.md ├── pub_test.go ├── netconn.go ├── mockconn_test.go ├── uniconn_test.go ├── uniconn.go └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | vendor 3 | *~ 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nknorg/mockconn-go 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/stretchr/testify v1.8.1 7 | golang.org/x/time v0.3.0 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/pmezard/go-difflib v1.0.0 // indirect 13 | gopkg.in/yaml.v3 v3.0.1 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /clientaddr.go: -------------------------------------------------------------------------------- 1 | package mockconn 2 | 3 | // ClientAddr represents MockConn endpoint address. It implements net.Addr interface. 4 | type ClientAddr struct { 5 | addr string 6 | } 7 | 8 | // NewClientAddr creates a ClientAddr from a client address string. 9 | func NewClientAddr(addr string) *ClientAddr { 10 | return &ClientAddr{addr: addr} 11 | } 12 | 13 | // Network returns "mockconn" 14 | func (addr ClientAddr) Network() string { return "mockconn" } 15 | 16 | // String returns the mockconn endpoint address string. 17 | func (addr ClientAddr) String() string { return addr.addr } 18 | -------------------------------------------------------------------------------- /mockconn.go: -------------------------------------------------------------------------------- 1 | package mockconn 2 | 3 | import ( 4 | "net" 5 | "time" 6 | ) 7 | 8 | // The config to mock out an connection 9 | // A connection has two address represent two endpoints Addr1 and Addr2. 10 | // Here we refer them as Addr1 and Addr2. They can be any string you would like. 11 | type ConnConfig struct { 12 | Addr1 string // endpoint 1 address 13 | Addr2 string // endpoint 2 address 14 | Throughput uint // throughput by packets/second 15 | BufferSize uint // BufferSize used int connection. If it is not set, a default value will be computed. 16 | Latency time.Duration // Latency is the duration which the packet travels from endpoint 1 to endpoint 2. 17 | Loss float32 // loss rate, 0.01 = 1% 18 | WriteTimeout time.Duration // set default timeout for writing, without timeout if zero 19 | ReadTimeout time.Duration // set default timeout for reading, without timeout if zero 20 | } 21 | 22 | // Mock network connection 23 | // Return two net.Conn(s) which represent two endpoints of this connection. 24 | func NewMockConn(conf *ConnConfig) (net.Conn, net.Conn, error) { 25 | l2r, err := NewUniConn(conf) 26 | if err != nil { 27 | return nil, nil, err 28 | } 29 | 30 | conf2 := *conf 31 | conf2.Addr1, conf2.Addr2 = conf2.Addr2, conf2.Addr1 // switch address 32 | r2l, err := NewUniConn(&conf2) 33 | if err != nil { 34 | return nil, nil, err 35 | } 36 | 37 | localEndpoint := NewNetConn(l2r, r2l) 38 | remoteEndpoint := NewNetConn(r2l, l2r) 39 | 40 | return localEndpoint, remoteEndpoint, nil 41 | } 42 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 5 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 6 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 7 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 8 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 9 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 10 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 11 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 12 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 13 | golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= 14 | golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 15 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 16 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 17 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 18 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 19 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mockconn 2 | 3 | Mock a network connection which has two endpoints to communicate. 4 | The connection implement of go standard library **net.Conn** interface 5 | 6 | This network connection has configurable parameters as : 7 | 8 | ``` 9 | type ConnConfig struct { 10 | Addr1 string 11 | Addr2 string 12 | Throughput uint 13 | BufferSize uint 14 | Latency time.Duration 15 | Loss float32 // 0.01 = 1% 16 | } 17 | ``` 18 | 19 | * Addr1: Address or any name to identify one endpoint, such as "Alice" or "127.0.0.1" 20 | * Addr2: Address or any name to identify the other endpoint, such as "Bob" or an IP address 21 | * Throughput: The Throughput (packet/second) you set for this connection. Each packet is default to 1024 bytes. 22 | * BufferSize: The buffer size used in the network. It is suggest equal or greater than throughput. 23 | * Latency: The duration which a packet travels in the connection. 24 | * Loss: The rate of loss in the connection. 25 | 26 | You can mock out a connection as: 27 | 28 | ``` 29 | conf := &ConnConfig{Addr1: "Alice", Addr2: "Bob", Throughput: uint(256), Latency: 100 * time.Millisecond} 30 | aliceConn, bobConn, err := NewMockConn(conf) 31 | ``` 32 | 33 | After mocking out the connection. You can begin send and receive data as: 34 | 35 | * Writer 36 | 37 | ``` 38 | b := make([]byte, 1024) 39 | // Todo: put meaningful data into b 40 | n, err := aliceConn.Write(b) 41 | if err != nil { 42 | return err 43 | } 44 | ``` 45 | 46 | * Reader 47 | 48 | ``` 49 | b := make([]byte, 1024) 50 | n, err := bobConn.Read(b) 51 | if err != nil { 52 | return ree 53 | } 54 | ``` 55 | 56 | After fininshing data transmitting and receiving all data, you may close this connection at both endpoints: 57 | 58 | ``` 59 | aliceConn.Close() 60 | bobConn.Close() 61 | ``` 62 | -------------------------------------------------------------------------------- /pub_test.go: -------------------------------------------------------------------------------- 1 | package mockconn 2 | 3 | import ( 4 | "encoding/binary" 5 | "log" 6 | "net" 7 | "time" 8 | ) 9 | 10 | func WritePacktes(writer net.Conn, nPackets int, sendCh chan []int64) { 11 | var sendSeq []int64 12 | 13 | seq := int64(1) 14 | count := int64(0) 15 | t1 := time.Now() 16 | for i := 0; i < nPackets; i++ { 17 | b := make([]byte, 1024) 18 | binary.PutVarint(b, seq) 19 | _, err := writer.Write(b) 20 | if err != nil { 21 | break 22 | } 23 | sendSeq = append(sendSeq, seq) 24 | seq++ 25 | count++ 26 | } 27 | dur := time.Since(t1) 28 | throughput := float64(count) / dur.Seconds() 29 | log.Printf("%v send %v packets in %v ms, throughput is: %.1f packets/s \n", 30 | writer.LocalAddr(), count, dur.Milliseconds(), throughput) 31 | 32 | sendCh <- sendSeq // return the sequences written 33 | } 34 | 35 | func ReadPackets(reader net.Conn, nPackets int, latency time.Duration, recvCh chan []int64) { 36 | var recvSeq []int64 37 | 38 | b := make([]byte, 1024) 39 | count := int64(0) 40 | t1 := time.Now() 41 | for i := 0; i < nPackets; i++ { 42 | _, err := reader.Read(b) 43 | if err != nil { 44 | break 45 | } 46 | seq, _ := binary.Varint(b[:8]) 47 | recvSeq = append(recvSeq, seq) 48 | count++ 49 | } 50 | dur := time.Since(t1) 51 | dur2 := dur - latency 52 | throughput := float64(count) / dur2.Seconds() 53 | 54 | log.Printf("%v read %v packets in %v ms, deduct %v ms latency, throughput is: %.1f packets/s \n", 55 | reader.LocalAddr(), count, dur.Milliseconds(), latency.Milliseconds(), throughput) 56 | 57 | if uc, ok := reader.(*UniConn); ok { 58 | uc.PrintMetrics() 59 | } else if nc, ok := reader.(*NetConn); ok { 60 | nc.PrintMetrics() 61 | } 62 | 63 | recvCh <- recvSeq // return the sequences read 64 | } 65 | -------------------------------------------------------------------------------- /netconn.go: -------------------------------------------------------------------------------- 1 | package mockconn 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | var ( 12 | ErrConnNotEstablished error = errors.New("NetConn is not established") 13 | ) 14 | 15 | type NetConn struct { 16 | sendConn *UniConn 17 | recvConn *UniConn 18 | 19 | readMu sync.RWMutex 20 | pauseRead bool 21 | writeMu sync.RWMutex 22 | pauseWrite bool 23 | } 24 | 25 | // An implement of net.Conn interface 26 | func NewNetConn(sendConn, recvConn *UniConn) *NetConn { 27 | nc := &NetConn{sendConn: sendConn, recvConn: recvConn} 28 | return nc 29 | } 30 | 31 | func (nc *NetConn) Write(b []byte) (n int, err error) { 32 | if nc.sendConn == nil { 33 | return 0, ErrConnNotEstablished 34 | } 35 | 36 | nc.writeMu.RLock() 37 | defer nc.writeMu.RUnlock() 38 | if nc.pauseWrite { 39 | return 0, errors.New("NetConn writing is paused") 40 | } 41 | 42 | return nc.sendConn.Write(b) 43 | } 44 | 45 | func (nc *NetConn) Read(b []byte) (n int, err error) { 46 | if nc.recvConn == nil { 47 | return 0, ErrConnNotEstablished 48 | } 49 | 50 | nc.readMu.RLock() 51 | defer nc.readMu.RUnlock() 52 | if nc.pauseRead { 53 | return 0, errors.New("NetConn reading is paused") 54 | } 55 | 56 | return nc.recvConn.Read(b) 57 | } 58 | 59 | func (nc *NetConn) Close() error { 60 | if nc.sendConn == nil || nc.recvConn == nil { 61 | return ErrConnNotEstablished 62 | } 63 | 64 | nc.sendConn.CloseWrite() 65 | nc.recvConn.CloseRead() 66 | return nil 67 | } 68 | 69 | func (nc *NetConn) CloseRead() error { 70 | return nc.recvConn.CloseRead() 71 | } 72 | 73 | func (nc *NetConn) CloseWrite() error { 74 | return nc.sendConn.CloseWrite() 75 | } 76 | 77 | func (nc *NetConn) LocalAddr() net.Addr { 78 | if nc.sendConn == nil { 79 | return nil 80 | } 81 | return nc.sendConn.LocalAddr() 82 | } 83 | 84 | func (nc *NetConn) RemoteAddr() net.Addr { 85 | if nc.sendConn == nil { 86 | return nil 87 | } 88 | return nc.sendConn.RemoteAddr() 89 | } 90 | 91 | func (nc *NetConn) SetDeadline(t time.Time) error { 92 | if nc.sendConn == nil { 93 | return ErrConnNotEstablished 94 | } 95 | if nc.recvConn == nil { 96 | return ErrConnNotEstablished 97 | } 98 | 99 | err := nc.sendConn.SetDeadline(t) 100 | if err != nil { 101 | return err 102 | } 103 | err = nc.recvConn.SetDeadline(t) 104 | if err != nil { 105 | return err 106 | } 107 | 108 | return nil 109 | } 110 | 111 | func (nc *NetConn) SetReadDeadline(t time.Time) error { 112 | if nc.recvConn == nil { 113 | return ErrConnNotEstablished 114 | } 115 | return nc.recvConn.SetReadDeadline(t) 116 | } 117 | 118 | func (nc *NetConn) SetWriteDeadline(t time.Time) error { 119 | if nc.sendConn == nil { 120 | return ErrConnNotEstablished 121 | } 122 | return nc.sendConn.SetWriteDeadline(t) 123 | } 124 | 125 | func (nc *NetConn) PrintMetrics() { 126 | if nc.recvConn == nil { 127 | return 128 | } 129 | nc.recvConn.PrintMetrics() 130 | } 131 | 132 | func (nc *NetConn) String() string { 133 | return fmt.Sprintf("NetConn endpoint %v", nc.LocalAddr()) 134 | } 135 | 136 | // The following functions are to better stimulating of network exceptions 137 | func (nc *NetConn) PauseRead() { 138 | nc.readMu.Lock() 139 | defer nc.readMu.Unlock() 140 | nc.pauseRead = true 141 | } 142 | 143 | func (nc *NetConn) ResumeRead() { 144 | nc.readMu.Lock() 145 | defer nc.readMu.Unlock() 146 | nc.pauseRead = false 147 | } 148 | 149 | func (nc *NetConn) PauseWrite() { 150 | nc.writeMu.Lock() 151 | defer nc.writeMu.Unlock() 152 | nc.pauseWrite = true 153 | } 154 | 155 | func (nc *NetConn) ResumeWrite() { 156 | nc.writeMu.Lock() 157 | defer nc.writeMu.Unlock() 158 | nc.pauseWrite = false 159 | } 160 | -------------------------------------------------------------------------------- /mockconn_test.go: -------------------------------------------------------------------------------- 1 | package mockconn 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "math" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | // go test -v -run=TestBidirection 14 | func TestBidirection(t *testing.T) { 15 | conf := &ConnConfig{Addr1: "Alice", Addr2: "Bob", Throughput: uint(256), Latency: 100 * time.Millisecond} 16 | log.Printf("Going to test bi-direction communicating, throughput is %v, latency is %v \n", 17 | conf.Throughput, conf.Latency) 18 | 19 | aliceConn, bobConn, err := NewMockConn(conf) 20 | require.NotNil(t, aliceConn) 21 | require.NotNil(t, bobConn) 22 | require.Nil(t, err) 23 | 24 | // Alice send to Bob 25 | nPackets := 100 26 | i := 0 27 | sendChan := make(chan []int64) 28 | recvChan := make(chan []int64) 29 | go WritePacktes(aliceConn, nPackets, sendChan) 30 | go ReadPackets(bobConn, nPackets, conf.Latency, recvChan) 31 | 32 | sendSeq := <-sendChan 33 | recvSeq := <-recvChan 34 | 35 | minLen := int(math.Min(float64(len(sendSeq)), float64(len(recvSeq)))) 36 | for i = 0; i < minLen; i++ { 37 | if sendSeq[i] != recvSeq[i] { 38 | log.Printf("%v sendSeq[%v] %v != %v recvSeq[%v] %v \n", 39 | aliceConn.LocalAddr(), i, sendSeq[i], bobConn.LocalAddr(), i, recvSeq[i]) 40 | } 41 | } 42 | if i == nPackets { 43 | log.Printf("%v write to %v %v packets, %v receive %v packets in the same sequence\n", 44 | aliceConn.LocalAddr(), bobConn.LocalAddr(), nPackets, bobConn.LocalAddr(), nPackets) 45 | } 46 | 47 | // Bob send to Alice 48 | nPackets = 50 49 | go WritePacktes(bobConn, nPackets, sendChan) 50 | go ReadPackets(aliceConn, nPackets, conf.Latency, recvChan) 51 | 52 | sendSeq = <-sendChan 53 | recvSeq = <-recvChan 54 | 55 | minLen = int(math.Min(float64(len(sendSeq)), float64(len(recvSeq)))) 56 | for i = 0; i < minLen; i++ { 57 | if sendSeq[i] != recvSeq[i] { 58 | log.Printf("%v sendSeq[%v] %v != %v recvSeq[%v] %v \n", 59 | bobConn.LocalAddr(), i, sendSeq[i], aliceConn.LocalAddr(), i, recvSeq[i]) 60 | } 61 | } 62 | if i == nPackets { 63 | log.Printf("%v write to %v %v packets, %v receive %v packets in the same sequence\n", 64 | bobConn.LocalAddr(), aliceConn.LocalAddr(), nPackets, aliceConn.LocalAddr(), nPackets) 65 | } 66 | } 67 | 68 | // go test -v -run=TestNetConnThroughput 69 | func TestNetConnThroughput(t *testing.T) { 70 | tpBase := uint(256) 71 | for i := 1; i <= 4; i++ { 72 | tp := tpBase * uint(i) 73 | conf := &ConnConfig{Addr1: "Alice", Addr2: "Bob", Throughput: tp, Latency: 20 * time.Millisecond} 74 | log.Printf("Going to test throughput at %v packets/s, latency %v\n", conf.Throughput, conf.Latency) 75 | 76 | aliceConn, bobConn, err := NewMockConn(conf) 77 | require.NotNil(t, aliceConn) 78 | require.NotNil(t, bobConn) 79 | require.Nil(t, err) 80 | 81 | nPackets := 256 82 | sendChan := make(chan []int64) 83 | recvChan := make(chan []int64) 84 | go WritePacktes(aliceConn, nPackets, sendChan) 85 | go ReadPackets(bobConn, nPackets, conf.Latency, recvChan) 86 | 87 | <-sendChan 88 | <-recvChan 89 | fmt.Println() 90 | } 91 | } 92 | 93 | // go test -v -run=TestHighLatency 94 | func TestHighLatency(t *testing.T) { 95 | conf := &ConnConfig{Addr1: "Alice", Addr2: "Bob", Throughput: uint(128), Latency: 500 * time.Millisecond} 96 | log.Printf("Going to test throughput at %v packets/s, high latency %v\n", conf.Throughput, conf.Latency) 97 | 98 | aliceConn, bobConn, err := NewMockConn(conf) 99 | require.NotNil(t, aliceConn) 100 | require.NotNil(t, bobConn) 101 | require.Nil(t, err) 102 | 103 | nPackets := 256 104 | sendChan := make(chan []int64) 105 | recvChan := make(chan []int64) 106 | go WritePacktes(aliceConn, nPackets, sendChan) 107 | go ReadPackets(bobConn, nPackets, conf.Latency, recvChan) 108 | 109 | <-sendChan 110 | <-recvChan 111 | } 112 | 113 | // go test -v -run=TestLoss 114 | func TestLoss(t *testing.T) { 115 | conf := &ConnConfig{Addr1: "Alice", Addr2: "Bob", Throughput: uint(128), Latency: 20 * time.Millisecond, Loss: 0.01} 116 | log.Printf("Going to test throughput at %v packets/s, latency %v, loss :%v \n", conf.Throughput, conf.Latency, conf.Loss) 117 | 118 | aliceConn, bobConn, err := NewMockConn(conf) 119 | require.NotNil(t, aliceConn) 120 | require.NotNil(t, bobConn) 121 | require.Nil(t, err) 122 | 123 | nPackets := 256 124 | sendChan := make(chan []int64) 125 | recvChan := make(chan []int64) 126 | go WritePacktes(aliceConn, nPackets, sendChan) 127 | go ReadPackets(bobConn, nPackets, conf.Latency, recvChan) 128 | 129 | <-sendChan 130 | aliceConn.Close() 131 | bobConn.Close() 132 | <-recvChan 133 | } 134 | -------------------------------------------------------------------------------- /uniconn_test.go: -------------------------------------------------------------------------------- 1 | package mockconn 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/require" 10 | "golang.org/x/time/rate" 11 | ) 12 | 13 | // go test -v -run=TestUniConnThroughput 14 | func TestUniConnThroughput(t *testing.T) { 15 | tpBase := uint(256) 16 | for i := 1; i <= 4; i++ { 17 | tp := tpBase * (uint)(i) 18 | conf := &ConnConfig{Addr1: "Alice", Addr2: "Bob", Throughput: tp, Latency: 20 * time.Millisecond} 19 | uc, err := NewUniConn(conf) 20 | require.Nil(t, err) 21 | require.NotNil(t, uc) 22 | 23 | fmt.Println("target tp is", tp) 24 | sendChan := make(chan []int64) 25 | recvChan := make(chan []int64) 26 | nPacket := 100 27 | go WritePacktes(uc, nPacket, sendChan) 28 | go ReadPackets(uc, nPacket, conf.Latency, recvChan) 29 | 30 | <-sendChan 31 | <-recvChan 32 | } 33 | } 34 | 35 | // go test -v -run=TestSetReadDeadline 36 | func TestSetReadDeadline(t *testing.T) { 37 | conf := &ConnConfig{Addr1: "Alice", Addr2: "Bob", Throughput: uint(16), Latency: 100 * time.Millisecond} 38 | uc, err := NewUniConn(conf) 39 | require.Nil(t, err) 40 | require.NotNil(t, uc) 41 | 42 | uc.SetReadDeadline(time.Now().Add(time.Second)) 43 | b := make([]byte, 1024) 44 | n, err := uc.Read(b) 45 | require.NotNil(t, err) 46 | require.Equal(t, 0, n) 47 | 48 | n, err = uc.Write(b) 49 | require.Nil(t, err) 50 | require.Equal(t, 1024, n) 51 | n, err = uc.Read(b) 52 | require.Nil(t, err) 53 | require.Equal(t, 1024, n) 54 | } 55 | 56 | // go test -v -run=TestClose 57 | func TestClose(t *testing.T) { 58 | conf := &ConnConfig{Addr1: "Alice", Addr2: "Bob", Throughput: uint(16), Latency: 100 * time.Millisecond} 59 | uc, err := NewUniConn(conf) 60 | require.Nil(t, err) 61 | require.NotNil(t, uc) 62 | 63 | b := []byte("hello") 64 | n, err := uc.Write(b) 65 | require.Nil(t, err) 66 | require.Equal(t, len(b), n) 67 | 68 | b2 := make([]byte, 1024) 69 | n, err = uc.Read(b2) 70 | 71 | require.Nil(t, err) 72 | require.Equal(t, len(b), n) 73 | 74 | uc.Write(b) 75 | uc.CloseRead() 76 | n, err = uc.Read(b2) 77 | require.NotNil(t, err) 78 | require.Equal(t, 0, n) 79 | t.Log("After close read, read err ", err) 80 | 81 | uc.CloseWrite() 82 | n, err = uc.Write(b) 83 | require.NotNil(t, err) 84 | require.Equal(t, 0, n) 85 | t.Log("After close write, write err ", err) 86 | } 87 | 88 | // go test -v -run=TestRateLimiter 89 | func TestRateLimiter(t *testing.T) { 90 | lim := 2000 91 | r := rate.NewLimiter(rate.Limit(lim), 1) 92 | count := 10000 93 | 94 | start := time.Now() 95 | for i := 0; i < count; i++ { 96 | r.Wait(context.Background()) 97 | } 98 | d := time.Since(start) 99 | 100 | fmt.Printf("Count %v took %v, average is %.1f, expected is %v\n", 101 | count, d, float64(count)/d.Seconds(), lim) 102 | } 103 | 104 | // go test -v -run=TestCloseRead 105 | func TestCloseRead(t *testing.T) { 106 | conf := &ConnConfig{Addr1: "Alice", Addr2: "Bob", Throughput: uint(16), Latency: 50 * time.Millisecond, WriteTimeout: 3 * time.Second} 107 | uc, err := NewUniConn(conf) 108 | require.Nil(t, err) 109 | require.NotNil(t, uc) 110 | 111 | writeCh := make(chan struct{}) 112 | go func() { 113 | i := 0 114 | for i = 0; i < 1000; i++ { 115 | b := make([]byte, 1024) 116 | n, err := uc.Write(b) 117 | if err != nil { 118 | t.Log("write err", err) 119 | break 120 | } else { 121 | require.Equal(t, n, len(b)) 122 | } 123 | } 124 | close(writeCh) 125 | }() 126 | 127 | go func() { 128 | i := 0 129 | for i = 0; i < 1000; i++ { 130 | b := make([]byte, 1024) 131 | n, err := uc.Read(b) 132 | if err != nil { 133 | t.Log("read err", err) 134 | break 135 | } else { 136 | require.Equal(t, n, len(b)) 137 | } 138 | 139 | if i == 100 { 140 | err := uc.CloseRead() 141 | if err != nil { 142 | t.Log("CloseRead err", err) 143 | } 144 | } 145 | } 146 | require.Equal(t, 101, i) 147 | }() 148 | 149 | <-writeCh 150 | } 151 | 152 | // go test -v -run=TestTimeout 153 | func TestTimeout(t *testing.T) { 154 | conf := &ConnConfig{Addr1: "Alice", Addr2: "Bob", Throughput: uint(16), Latency: 20 * time.Millisecond, 155 | WriteTimeout: 2 * time.Second, ReadTimeout: 3 * time.Second} 156 | uc, err := NewUniConn(conf) 157 | require.Nil(t, err) 158 | require.NotNil(t, uc) 159 | 160 | for i := 0; i < 1000; i++ { 161 | b := []byte("hello") 162 | n, err := uc.Write(b) 163 | if err != nil { 164 | require.Equal(t, 0, n) 165 | require.Equal(t, context.DeadlineExceeded, err) 166 | break 167 | } 168 | } 169 | 170 | for i := 0; i < 1000; i++ { 171 | b := make([]byte, 1024) 172 | n, err := uc.Read(b) 173 | if err != nil { 174 | require.Equal(t, 0, n) 175 | require.Equal(t, context.DeadlineExceeded, err) 176 | break 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /uniconn.go: -------------------------------------------------------------------------------- 1 | package mockconn 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "math/rand" 9 | "net" 10 | "time" 11 | 12 | "golang.org/x/time/rate" 13 | ) 14 | 15 | var ( 16 | zeroTime time.Time 17 | ErrClosedConn error = errors.New("connection is closed") 18 | ErrNilPointer error = errors.New("data pointer is nil") 19 | ErrZeroLengh error = errors.New("zero length data to write") 20 | ErrUnknown error = errors.New("UniConn unknown error") 21 | ) 22 | 23 | // To trace time consuming. 24 | type dataWithTime struct { 25 | data []byte 26 | t time.Time 27 | } 28 | 29 | // unidirectional channel, can only send data from localAddr to remoteAddr 30 | type UniConn struct { 31 | localAddr string 32 | remoteAddr string 33 | 34 | throughput uint 35 | bufferSize uint 36 | latency time.Duration 37 | loss float32 38 | writeTimeout time.Duration // default timeout for writing 39 | readTimeout time.Duration // default timeout for reading 40 | 41 | sendCh chan *dataWithTime 42 | bufferCh chan *dataWithTime 43 | recvCh chan *dataWithTime 44 | 45 | unreadData []byte // save unread data 46 | 47 | // for metrics 48 | nSendPacket int64 // number of packets sent 49 | nRecvPacket int64 // number of packets received 50 | nLoss int64 // number of packets are random lost 51 | averageLatency time.Duration // average latency of all packets 52 | 53 | // one time deadline and cancel 54 | readCtx context.Context 55 | readCancel context.CancelFunc 56 | writeCtx context.Context 57 | writeCancel context.CancelFunc 58 | 59 | // close UniConn 60 | closeWriteCtx context.Context 61 | closeWriteCtxCancel context.CancelFunc 62 | closeReadCtx context.Context 63 | closeReadCtxCancel context.CancelFunc 64 | } 65 | 66 | func init() { 67 | rand.Seed(time.Now().UnixNano()) 68 | } 69 | 70 | func NewUniConn(conf *ConnConfig) (*UniConn, error) { 71 | bufferSize := conf.BufferSize 72 | if bufferSize == 0 { 73 | bufferSize = uint(2 * float64(conf.Throughput) * conf.Latency.Seconds()) 74 | } 75 | 76 | uc := &UniConn{throughput: conf.Throughput, bufferSize: bufferSize, latency: conf.Latency, loss: conf.Loss, 77 | writeTimeout: conf.WriteTimeout, readTimeout: conf.ReadTimeout, 78 | sendCh: make(chan *dataWithTime), bufferCh: make(chan *dataWithTime, bufferSize), 79 | recvCh: make(chan *dataWithTime), localAddr: conf.Addr1, remoteAddr: conf.Addr2} 80 | 81 | uc.closeWriteCtx, uc.closeWriteCtxCancel = context.WithCancel(context.Background()) 82 | uc.closeReadCtx, uc.closeReadCtxCancel = context.WithCancel(context.Background()) 83 | uc.SetDeadline(zeroTime) 84 | 85 | go uc.throughputRead() 86 | go uc.latencyRead() 87 | 88 | return uc, nil 89 | } 90 | 91 | func (uc *UniConn) Write(b []byte) (n int, err error) { 92 | if err = uc.writeCtx.Err(); err != nil { 93 | return 0, err 94 | } 95 | 96 | if len(b) == 0 { 97 | return 0, ErrZeroLengh 98 | } 99 | 100 | var timeoutCtx context.Context 101 | var timeoutCancel context.CancelFunc 102 | if uc.writeTimeout > 0 { 103 | timeoutCtx, timeoutCancel = context.WithTimeout(uc.writeCtx, uc.writeTimeout) 104 | } else { 105 | timeoutCtx, timeoutCancel = context.WithCancel(uc.writeCtx) 106 | } 107 | defer timeoutCancel() 108 | 109 | dt := &dataWithTime{data: b} 110 | select { 111 | case uc.sendCh <- dt: 112 | uc.nSendPacket++ 113 | 114 | case <-timeoutCtx.Done(): 115 | return 0, timeoutCtx.Err() 116 | } 117 | 118 | return len(b), nil 119 | } 120 | 121 | func (uc *UniConn) randomLoss() bool { 122 | if uc.loss > 0 { 123 | l := rand.Float32() 124 | if l < uc.loss { 125 | uc.nLoss++ 126 | return true 127 | } 128 | } 129 | return false 130 | } 131 | 132 | // The routine to stimulate throughput by rate Limiter 133 | func (uc *UniConn) throughputRead() error { 134 | defer close(uc.bufferCh) 135 | 136 | r := rate.NewLimiter(rate.Limit(uc.throughput), 1) 137 | for { 138 | err := r.Wait(uc.closeWriteCtx) 139 | if err != nil { 140 | return err 141 | } 142 | 143 | select { 144 | case <-uc.closeWriteCtx.Done(): 145 | return uc.closeWriteCtx.Err() 146 | 147 | case dt := <-uc.sendCh: 148 | if dt != nil { 149 | if !uc.randomLoss() { 150 | dt.t = time.Now() 151 | uc.bufferCh <- dt 152 | } 153 | } 154 | } 155 | } 156 | } 157 | 158 | // The routine to stimulate latency 159 | func (uc *UniConn) latencyRead() error { 160 | defer close(uc.recvCh) 161 | for { 162 | select { 163 | case <-uc.closeReadCtx.Done(): 164 | return uc.closeReadCtx.Err() 165 | 166 | case dt := <-uc.bufferCh: 167 | if dt != nil { 168 | dur := time.Since(dt.t) 169 | if dur < uc.latency { 170 | timer := time.NewTimer(uc.latency - dur) 171 | select { 172 | case <-uc.closeReadCtx.Done(): 173 | return uc.closeReadCtx.Err() 174 | 175 | case <-timer.C: 176 | } 177 | } 178 | uc.recvCh <- dt 179 | } 180 | } 181 | } 182 | } 183 | 184 | func (uc *UniConn) Read(b []byte) (n int, err error) { 185 | if err = uc.readCtx.Err(); err != nil { 186 | return 0, err 187 | } 188 | 189 | // check buffered unread data 190 | unreadLen := len(uc.unreadData) 191 | if unreadLen > 0 { 192 | if unreadLen <= len(b) { 193 | copy(b, uc.unreadData) 194 | uc.unreadData = make([]byte, 0) 195 | return unreadLen, nil 196 | } else { 197 | copy(b, uc.unreadData[0:len(b)]) 198 | uc.unreadData = uc.unreadData[len(b):] 199 | return len(b), nil 200 | } 201 | } 202 | 203 | var timeoutCtx context.Context 204 | var timeoutCancel context.CancelFunc 205 | if uc.readTimeout > 0 { 206 | timeoutCtx, timeoutCancel = context.WithTimeout(uc.readCtx, uc.readTimeout) 207 | } else { 208 | timeoutCtx, timeoutCancel = context.WithCancel(uc.readCtx) 209 | } 210 | defer timeoutCancel() 211 | 212 | for { 213 | if err := uc.readCtx.Err(); err != nil { 214 | return 0, err 215 | } 216 | 217 | select { 218 | case dt := <-uc.recvCh: 219 | if dt != nil { 220 | if len(dt.data) > len(b) { 221 | dt.data = dt.data[0:len(b)] 222 | n = len(b) 223 | uc.unreadData = dt.data[len(b):] 224 | } else { 225 | n = len(dt.data) 226 | } 227 | 228 | copy(b, dt.data) 229 | uc.nRecvPacket++ 230 | 231 | uc.averageLatency = time.Duration(float64(uc.averageLatency)*(float64(uc.nRecvPacket-1)/float64(uc.nRecvPacket)) + 232 | float64(time.Since(dt.t))/float64(uc.nRecvPacket)) 233 | 234 | return n, nil 235 | } 236 | 237 | case <-timeoutCtx.Done(): 238 | return 0, timeoutCtx.Err() 239 | } 240 | } 241 | } 242 | 243 | func (uc *UniConn) CloseWrite() error { 244 | uc.closeWriteCtxCancel() 245 | close(uc.sendCh) 246 | return nil 247 | } 248 | 249 | func (uc *UniConn) CloseRead() error { 250 | uc.closeReadCtxCancel() 251 | return nil 252 | } 253 | 254 | func (uc *UniConn) Close() error { 255 | uc.CloseWrite() 256 | uc.CloseRead() 257 | return nil 258 | } 259 | 260 | func (uc *UniConn) LocalAddr() net.Addr { 261 | return ClientAddr{addr: uc.localAddr} 262 | } 263 | 264 | func (uc *UniConn) RemoteAddr() net.Addr { 265 | return ClientAddr{addr: uc.remoteAddr} 266 | } 267 | 268 | func (uc *UniConn) SetDeadline(t time.Time) error { 269 | err := uc.SetReadDeadline(t) 270 | if err != nil { 271 | return err 272 | } 273 | err = uc.SetWriteDeadline(t) 274 | if err != nil { 275 | return err 276 | } 277 | return nil 278 | } 279 | 280 | func (uc *UniConn) SetReadDeadline(t time.Time) error { 281 | if t == zeroTime { 282 | uc.readCtx, uc.readCancel = context.WithCancel(uc.closeReadCtx) 283 | } else { 284 | uc.readCtx, uc.readCancel = context.WithDeadline(uc.closeReadCtx, t) 285 | } 286 | return nil 287 | } 288 | 289 | func (uc *UniConn) SetWriteDeadline(t time.Time) error { 290 | if t == zeroTime { 291 | uc.writeCtx, uc.writeCancel = context.WithCancel(uc.closeWriteCtx) 292 | } else { 293 | uc.writeCtx, uc.writeCancel = context.WithDeadline(uc.closeWriteCtx, t) 294 | } 295 | return nil 296 | } 297 | 298 | func (uc *UniConn) PrintMetrics() { 299 | log.Printf("%v to %v, %v packets are sent, %v packets are received, %v packets are lost, average latency is %v, loss rate is %.3f\n", 300 | uc.localAddr, uc.remoteAddr, uc.nSendPacket, uc.nRecvPacket, uc.nLoss, uc.averageLatency, float64(uc.nLoss)/float64(uc.nRecvPacket)) 301 | } 302 | 303 | func (uc *UniConn) String() string { 304 | return fmt.Sprintf("UniConn from %v to %v", uc.localAddr, uc.remoteAddr) 305 | } 306 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------