Go语言系列- Socket编程和Redis

时间:2023-03-08 18:16:01

Socket编程

一、socket编程概述

  什么是socket编程? socket编程是计算机PC机器上2个程序通过一个双向的通信连接实现数据的交互,这个连接的一端就是一个socket。socket的翻译意思上还有个插座的概念,其实,也可以很形象的比喻为插座插上去了就有通电了(网络通了)。socket编程其实作为UNIX系统的进程间通信机制,通常称为“套接字”,用来描述IP地址和端口的集合,在unix系统下是一个通信的句柄(文件描述符,因为UNIX下所有都是文件)。

UNIX socket编程的流程大概如下:

  • 服务端:socket(),bind(),listen(),accept(),read()/write(),close()
  • 客户端:socket(), connect(),read()/write(),close()

二、Go语言socket编程库

go语言的基础包中,网络包net包含了很多网络I/O,TCP/IP,UDP,域名解析,Unix 域套接字等。

虽然提供的都是直接面对很底层的网络I/O访问,但是主要的TCP通信的接口也是封装得比较简单,好用,包括有:

  • 客户端连接使用的Dial方法
  • 服务端进行监听使用的Listen方法
  • 服务端进行接受链接的Accept方法
  • 封装了连接对象Conn类型

三、客户端和服务端

Go语言系列- Socket编程和Redis

1. 服务器处理流程

  • a. 监听端口

  • b. 接收客户端的链接

  • c. 创建goroutine,处理该链接

2.  客户端处理流程

  • a. 建立与服务器的链接

  • b. 进行数据收发

  • c. 关闭链接

3. 服务端代码

package main

import (
"fmt"
"net"
) 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, 512)
n, err := conn.Read(buf)
if err != nil {
fmt.Println("read err:", err)
return
} fmt.Printf(string(buf[0:n]))
}
}

4. 客户端代码

package main

import (
"bufio"
"fmt"
"net"
"os"
"strings"
) func main() { 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
}
}
}

5. 发送http请求

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:keep-alive\r\n"
// msg += "User-Agent:Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36\r\n"
msg += "\r\n\r\n" //io.WriteString(os.Stdout, msg)
n, err := io.WriteString(conn, msg)
if err != nil {
fmt.Println("write string failed, ", err)
return
}
fmt.Println("send to baidu.com bytes:", n)
buf := make([]byte, 4096)
for {
count, err := conn.Read(buf)
fmt.Println("count:", count, "err:", err)
if err != nil {
break
}
fmt.Println(string(buf[0:count]))
}
}

 Redis

一、Redis简介

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

1. 特点

  • 支持更多数据类型 
    和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set 有序集合)和hash(哈希类型)。[1]

  • 支持复杂操作 
    这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。在此基础上,Redis支持各种不同方式的排序。[2]

  • 支持主从同步。 
    与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是Redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。数据可以从主服务器向任意数量的从服务器上同步,从服务器可以是关联其他从服务器的主服务器。这使得Redis可执行单层树复制。从盘可以有意无意的对数据进行写操作。由于完全实现了发布/订阅机制,使得从数据库在任何地方同步树时,可订阅一个频道并接收主服务器完整的消息发布记录。同步对读取操作的可扩展性和数据冗余很有帮助。

2.  常见使用场景

  • 高并发下数据缓存。 比如在某个场景下,大量日志同时写入数据库会给服务器带来巨大压力,这时可以先将数据写入redis中,再由redis写入数据库,减轻同时写入压力。
  • 热点信息快速显示。假设现在有一个新闻首页,需要快速显示各栏目前20条热点新闻,如果直接查询数据库,在大量用户同时访问下,会消耗极大数量的数据库请求。这时就可以用redis来优化,在新闻录入的时候将标题、时间和来源写入redis中,客户端访问时,可以从内存中一次性取出当天热单新闻列表,极大地提高请求速度和节约了服务器开销。
  • 保存会话信息。可以将登录后用户信息缓存入redis并同时设置key过期时间,这样后台api过滤请求时,就可以从内存中读取用户信息,而且redis的过期机制,天然支持用户身份有效期校验,用起来十分方便。
  • 统计计数。比如系统中常见一个功能是限制同一用户固定时间段内的登录次数或者所有请求次数,这时就可以以用户id为key,次数值为value,将计数信息缓存起来,并且有INCRBY命令原生支持。
  • 其他。Redis的应用场景十分广发,队列、发布订阅、统计分析等等,可以看看其他文章的介绍说明。

  Redis的出现,很大程度补偿了memcached这类key/value存储的不足,在部 分场合可以对关系数据库起到很好的补充作用。它提供了Java,C/C++,C#,PHP,JavaScript,Perl,Object-C,Python,Ruby,Erlang等客户端,使用很方便。Redis的官网地址,非常好记,是redis.io。目前,Vmware在资助着Redis项目的开发和维护。

二、Redis的主要数据结构

  Redis主要有五种基本数据结构,满足了绝大多数缓存结构的需要,如果你在使用一种结构存储时感觉别扭时,很有可能是选错了存储结构,可以考虑一下其他结构的正确实现。

  • String ,可以是字符串、整数和浮点数。如果是序列化数据,并涉及到修改操作的话,不推荐用string,可以考虑用Hash
  • Hash, key-value 对象,可以存放对象数据,比如用户信息之类。
  • List,有序数据集合,元素可以重复,用LPUSHLPOPRPUSHRPOP等指令组合可以实现栈和队列操作。
  • Set,无序集合,元素唯一。
  • Sorted Set,Sort的有序版,可以设定Score值来决定元素排序,适合用户排名这样的业务场景。

二、Redis的使用

1. redigo

  redigo是GO语言的一个redis客户端实现。项目位于https://github.com/garyburd/redigo

2. 安装redigo

redigo没有其他依赖项,可以直接通过go get进行安装

go get github.com/garyburd/redigo/redis

3. 连接redis

package main

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

4. 建立连接池

  Redigo Pool 结构维护一个 Redis 连接池。应用程序调用 Get 方法从池中获取连接,并使用连接的 Close 方法将连接的资源返回到池中。一般我们在系统初始化时声明一个全局连接池,然后在需要操作redis时获得连接,执行指令。

pool := &redis.Pool{
MaxIdle: 3, /*最大的空闲连接数*/
MaxActive: 8, /*最大的激活连接数*/
Dial: func() (redis.Conn, error) {
c, err := redis.Dial("tcp", "链接地址,例如127.0.0.1:6379", redis.DialPassword("密码"))
if err != nil {
return nil, err
}
return c, nil
},
}
c:=pool.Get()
defer c.Close()
Go语言系列- Socket编程和RedisGo语言系列- Socket编程和Redis
package main

import (
"fmt" "github.com/garyburd/redigo/redis"
) var pool *redis.Pool func init() {
pool = &redis.Pool{
MaxIdle: 16, /* 最大的空闲连接数 */
MaxActive: 0, /* 最大的激活连接数 */
IdleTimeout: 300,
Dial: func() (redis.Conn, error) {
c, err := redis.Dial("tcp", "localhost:6379", redis.DialPassword("0000"))
if err != nil {
return nil, err
}
return c, nil
},
}
} func main() { c := pool.Get()
defer c.Close() _, err := c.Do("Set", "abc", 100)
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)
pool.Close()
}

redis连接池实例

5.  执行指令

  查看源码,发现Conn 接口有一个执行 Redis 命令的通用方法:

//gomodule/redigo/redis/redis.go

// Conn represents a connection to a Redis server.
type Conn interface {
// Close closes the connection.
Close() error // Err returns a non-nil value when the connection is not usable.
Err() error // Do sends a command to the server and returns the received reply.
Do(commandName string, args ...interface{}) (reply interface{}, err error) // Send writes the command to the client's output buffer.
Send(commandName string, args ...interface{}) error // Flush flushes the output buffer to the Redis server.
Flush() error // Receive receives a single reply from the Redis server
Receive() (reply interface{}, err error)
}

http://redis.io/commands 中的 Redis 命令参考列出了可用的命令。do的参数和redis-cli命令参数格式一致,比如SET key value EX 360 对应函数调用为Do("SET", "key", "value","EX",360),常用的命令示例有:  

c := pool.Get()
defer c.Close()
//存值,
_, err := c.Do("SET", "key", "value")
//设置过期时间
_, err := c.Do("SET", "key", "value","EX",360)
//存int
_, err := c.Do("SET", "key", 2) //取值
v,err:=redis.String(c.Do("GET","key"))
bytes, err := redis.Bytes(c.Do("GET", "key"))
Go语言系列- Socket编程和RedisGo语言系列- Socket编程和Redis
package main

import (
"fmt" "github.com/garyburd/redigo/redis"
) func main() {
var p *int
var a int
p = &a
*p = 0 c, err := redis.Dial("tcp", "localhost:6379")
if err != nil {
fmt.Println("conn redis failed,", err)
return
} defer c.Close()
_, err = c.Do("Set", "abc", 100)
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)
}

set操作

Go语言系列- Socket编程和RedisGo语言系列- Socket编程和Redis
package main

import (
"fmt" "github.com/garyburd/redigo/redis"
) func main() {
c, err := redis.Dial("tcp", "localhost:6379")
if err != nil {
fmt.Println("conn redis failed,", err)
return
} defer c.Close()
_, err = c.Do("MSet", "abc", 100, "efg", 300)
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)
}
}

mset操作

Go语言系列- Socket编程和RedisGo语言系列- Socket编程和Redis
package main

import (
"fmt" "github.com/garyburd/redigo/redis"
) func main() {
c, err := redis.Dial("tcp", "localhost:6379")
if err != nil {
fmt.Println("conn redis failed,", err)
return
} defer c.Close()
_, err = c.Do("HSet", "books", "abc", 100)
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)
}

hset操作

Go语言系列- Socket编程和RedisGo语言系列- Socket编程和Redis
package main

import (
"fmt" "github.com/garyburd/redigo/redis"
) func main() {
c, err := redis.Dial("tcp", "localhost:6379")
if err != nil {
fmt.Println("conn redis failed,", err)
return
} defer c.Close()
_, err = c.Do("lpush", "book_list", "abc", "ceg", 300)
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)
}

list队列操作

Go语言系列- Socket编程和RedisGo语言系列- Socket编程和Redis
package main

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

设置过期时间