Go-tcp编程和Redis

tcp编程

1.网络编程基本介绍

Golang的主要设计目标之一就是面向大规模后端服务程序,网络通信是服务端程序必不可少的也是至关重要的一部分。
网络编程有两种:
1)Tcp socket(tcp 套接字)编程(c/s结构),是网络编程的主流。之所以加Tcp socket编程,是因为底层是基于Tcp/ip协议的,例如:QQ聊天
2)b/s结构的http编程,使用浏览器去访问服务器时,使用的就是http协议,而http底层依旧是用tcp socket实现的。例如:京东商城【属于go web开发范畴】

1.1 网络编程基础知识
(1)协议(tcp/ip)
Tcp/Ip(Transmission Control Protocol/Internet Protocol)的简写,中文译名为传输控制协议/因特网互联协议,又称为网络通讯协议,这个协议是Internet最基本的协议、Internet国际互联网的基础,简而言之,就是由网络层的IP协议和传输层的TCP协议组成的。
(2)Tcp/Ip模型
现实开发中,采用的Tcp/Ip模型包括四层结构:
应用层(application):smtp,ftp,telnet http
传输层(transport):解释数据
网络层:(ip)定位IP地址和确定连接路径
链路层:(link)与硬件驱动对话

Go-tcp编程和Redis_第1张图片
Go-tcp编程和Redis_第2张图片

(3)IP地址
概述:每个Internet上的主机和路由器都有一个ip地址,它包括网络号和主机号,IP地址有ipv4(32位)和ipv6(128位)。可以通过ipconfig来查看。
(4)端口(port)介绍
非物理意义上的端口/连接处,而是特指tcp/ip协议中的端口,是逻辑意义上的端口。
如果把ip地址比作成一个房间,端口就是出入这间房的门。实际的房间最多只有几个门,但是一个ip地址的端口可以有65536(即256*256)个之多!端口是通过端口号来标识的,端口号只有整数,范围从0-65535。需注意不是所有的端口号都能在开发中使用,例如端口号为0的端口就很特殊。可以通过netstat -a查看占用端口号。

Go-tcp编程和Redis_第3张图片

端口(port)分类:
1)0:是保留端口(不能用)
2)1-1024固定端口又称为有名端口,即被某些程序固定使用,一般程序员不使用。例如端口号22:SSH远程登录协议,23:telnet使用;21:ftp使用;25:smtp服务使用;80:iis使用;7:echo服务
3)1025-65535为动态端口,这些端口在编程时可以使用

端口(port)使用注意事项:
1)在计算机(尤其是做服务器)要尽量少开端口
2)一个端口只能被一个程序监听
3)如果使用netstat -an 可以查看本机有哪些端口在监听
4)可以使用netstat -anb来查看监听端口的pid,结合任务管理器关闭不安全的端口

2.tcp socket编程

下图为Golang中 tcp socket编程中的客户端和服务器的网络分布。(tcp链接是长链接)

Go-tcp编程和Redis_第4张图片

2.1 tcp socket编程快速入门

服务端的处理流程
1)监听端口
2)接收客户创建的tcp链接,建立客户端和服务器端的链接
3)创建goroutine,处理该链接的请求(通常客户端会通过链接发送请求包)

客户端的处理数据
1)建立与服务器间的链接
2)发送请求数据,接收服务器端发送的结果数据
3)关闭链接

Go-tcp编程和Redis_第5张图片
远程测试
telnet
例如想测试百度的80端口是否在正常运行telnet www.baidu.com 80
退出使用control +] 再输入quit
查找自己的ip地址
使用ipconfig

代码的实现

  • 服务器端功能:
    1.编写一个服务器端程序,在8888端口监听
    可以和多个客户创建链接
    链接成功后,客户端可以发送数据,服务器端接收数据,并显示在终端上。
    先使用telnet来测试,然后编写客户端来测试

服务端的代码: server.go

package main

import (
	"fmt"
	"io"
	"log"
	"net"
)

func process(conn net.Conn) {
	// 这里循环接收客户端的发送的数据
	defer conn.Close() //关闭conn
	for {
		// 创建一个新的切片,防止切片后续时出现各种问题
		buf := make([]byte, 1024)
		// conn.Reade(buf)
		// 1.等待客户端通过conn发送信息
		// 2.如果客户端没有write[发送],那么协程就阻塞在这里或者等待超时,自动关闭申请
		fmt.Printf("server are waiting for the response of client%s\t", conn.RemoteAddr().String())
		n, err := conn.Read(buf) //从conn读取
		if err != nil {
			if err == io.EOF {
				fmt.Println("server was closed")
			} else {
				fmt.Println("server 的Read err=", err)
			}
			return
		}
		// 3. 显示客户端发送的内容到服务器的终端
		fmt.Print(string(buf[:n]))//限定字节数十分重要
	}
}

func main() {
	fmt.Println("服务器开始监听...")
	// net.Listen("tcp", "0.0.0.0:88
	// 1.tcp 表示使用网络协议tcp
	// 2. 0.0.0.0:8888(IPV4和IPV6都适用) 表示在本地监听8888端口
	listen, err := net.Listen("tcp", "0.0.0.0:8888")
	if err != nil {
		fmt.Println("listen err=", err)
		log.Fatal(err)
	}
	defer listen.Close() //延时关闭

	// 为防止程序直接往下执行,跳出程序
	// 循环等待客户来链接
	for {
		// 等待客户端来链接
		fmt.Println("等待客户来链接")
		conn, err := listen.Accept()
		if err != nil {
			// log.Fatal(err) //此处最好不要用log 因为一个链接出错了,可能其他大量链接是正常的
			fmt.Println("Accept() err=", err)
			return
		} else {
			fmt.Printf("Accept() success conn=%v 本地端ip=%v 客户端ip=%v\n", conn, conn.LocalAddr().String(), conn.RemoteAddr().String())
		}
		// 这里准备起一个协程,为客户端服务
		go process(conn)
	}

	// fmt.Printf("listen success =%v\n", listen)

}

  • 客户端功能:
    1.编写一个客户端程序,能链接到服务器端的8888端口
    2.客户端可以发送单行数据,然后就退出
    3.能通过终端输入数据(输入一行发送一行),并发送给服务器端口
    4.在终端输入exit表示退出程序

客户端的代码:client.go

package main

import (
	"bufio"
	"fmt"
	"net"
	"os"
	"strings"
)

func main() {
	var total int
	conn, err := net.Dial("tcp", "10.141.105.8:8888")
	if err != nil {
		fmt.Println("client dial err=", err)
		return
	}
	// fmt.Println("conn success :", conn)
	// 功能一:客户端可以发送单行数据,然后就退出
	for {
		reader := bufio.NewReader(os.Stdin) //os.Stin 代表标准输入【终端】

		//从终端读取一行用户输入,并准备发送给服务器
		str, err := reader.ReadString('\n')
		if err != nil {
			fmt.Println("reader readString err=", err)
			return
		}

		// 如果用户输入的是exit就退出
		// 由于此时的str是带有“/r/n“的需要在进行比较是去除“/r/n“

		if flag := strings.Trim(str, "\r\n"); flag == "exit" {
			fmt.Println("客户端退出。。。")
			fmt.Printf("客户端发送了%d字节的数据,并退出\n", total)
			break
		}
		// 再将str 发送给服务器
		n, err := conn.Write([]byte(str)) //n写入的字节数
		if err != nil {
			fmt.Println("conn.Write err=", err)
		}
		total += n
	}
}

3.海量用户即时通讯系统

3.1项目开发流程

需求分析——>设计阶段——>编码实现——>测试阶段——>实施
需求分析:

  • 用户注册
  • 用户登录
  • 显示在线用户列表
  • 群聊(广播)
  • 点对点聊天
  • 离线留言

3.2界面设计Go-tcp编程和Redis_第6张图片

3.3 项目开发前技术准备

项目需要保存用户信息和消息数据,因此需要运用到数据库(Redis或MySQL)的知识,先了解在Golang中运用Redis。
Go-tcp编程和Redis_第7张图片

4. Redis

4.1 Redis基本介绍

1.Redis是NoSQL数据库,不是传统的关系型数据库。(官网http://www.redis.cn)
2.Redis全称为Remote Dictionary Server(远程字典服务器),Redis性能非常高,单机能够达到15w qps,通常适合做缓存,也可以持久化
3.是完全开源免费的,高性能的(key/value)分布式内存数据库,基于内存运行并支持持久化的NoSQL数据库,是最热门的NoSQL数据库之一,也称为数据结构服务器
4.Redis基本原理图
Go-tcp编程和Redis_第8张图片

5.redis的命令( http://redisdoc.com
Redis安装好后,默认有16个数据库,初始使用0号数据库,编号0…15
(1)添加key-val 【set】
(2)查看当前Redis的所有key【keys *】
(3)获取key对应的值【get key】
(4)切换Redis数据库【select index】
(5)查看当前数据库的key-val数量【dbsize】
(6)清空当前数据库的key-val和清空所有数据库的数据库的key-val 【flushdb flushall】

4.2 Redis的Crud操作

Redis支持的五大数据类型:String(字符串)、Hash(哈希)、List(列表)、Set(集合)、zset(sorted set 有序集合)。( 更多细节见http://redisdoc.com

String(字符串):

  • String是Redis最基本的数据类型,一个key对应一个value。
  • String类型是二进制的、安全的。原则上,除了普通的字符串,也可以存放图片等数据。
    Redis中一个字符串value容量最大是512M。

举例:存放一个地址信息,key:address ; value: beijin
在这里插入图片描述

  • String的Crud:set[如果存在就相当于修改,不存在就是添加]/get/del
    在这里插入图片描述
  • 常用指令:
    (1)setex(set with expire):setex key seconds valus。该命令类似于:set key value+expire value seconds。当key已经存在,SETE命令将覆写旧值。
    Go-tcp编程和Redis_第9张图片
    (2)mset【同时设置一个或多个key-val对】(如果存在已有的key,则覆写旧值 );mget【同时获取一个或多个val值】。
    例如:同时设置两个工人的名字,并同时获得
    Go-tcp编程和Redis_第10张图片

**Hash(哈希,类似golang中的Map): **

Redis hash 是一个键值对集合。类似于var user1 map[string]string
Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象/结构体,且hash的key值是唯一的。

指令代码:hset \ hget创建和获取Hash类型信息。
举例说明:
存放一个User信息,user1 name “Smith” age 25 job “coder”。Key:user1; 三对filed-val:name “Smith” ;age 25 ;job “coder”
Go-tcp编程和Redis_第11张图片
hgetall获得所有hash表key中的所有域和值。
hgetall key
Go-tcp编程和Redis_第12张图片
hdel删除hash表中key的某个或多个域,如果域不存在将会被忽略。
hdel key field
在这里插入图片描述

**hmset \ hmget同时将多个field-val设置到hash表中\返回hash表中一个或多个指定域的值。
Go-tcp编程和Redis_第13张图片
hlen统计hash表中的字段。
hlen key
Go-tcp编程和Redis_第14张图片
hexists判断hash表中key是否有存在某个字段,存在时返回1,不存在返回0。
hexists key field
在这里插入图片描述


List(列表):

  • 列表是简单的字符串列表,按照插入顺序排序。可以添加一个元素到表的头部(左边)或者尾部(右边)

  • List的本质是个链表,List的元素是有序的,元素的值是可以重复的

  • List,不论是从左或是从右推入数据,其元素间的指向都是从左到右的关系

  • List中的元素全移除,对应的键也就消失了。
    Go-tcp编程和Redis_第15张图片

指令代码:

  • lpush将一个或多个value插入到列表key的标头,遵循先入后出的方式。lrange返回列表 key 中指定区间内的元素,区间以偏移量 start 和 stop 指定。(0表示表左边第一个元素,-1表示表右边第一个元素)
    举例:存放多个地址信息,city 北京 天津 上海
    Go-tcp编程和Redis_第16张图片
  • rpush将一个或多个值 value 插入到列表 key 的表尾(最右边)。
    Go-tcp编程和Redis_第17张图片
  • Lpop移除并返回列表key的头元素。Rpop移除并返回列表 key 的尾元素。
    Go-tcp编程和Redis_第18张图片
  • del 删除,del key 直接删除列表
  • Lset将列表 key 下标为 index 的元素的值设置为 value 。
    Go-tcp编程和Redis_第19张图片
  • LindexLindex key index返回列表 key 中,下标为 index 的元素。
  • LLen:返回列表key的长度。
    Go-tcp编程和Redis_第20张图片

Set(集合):

  • Redis的Set是string类型的无序集合
  • 底层是Hash Table数据结构,Set也是存放很多字符串元素,字符串元素是无序的,而且元素值不能重复

用途:用于存放唯一形式/独一份的数据,例如电子邮件
Go-tcp编程和Redis_第21张图片
指令代码:

  • Sadd:将一个或多个 member 元素加入到集合 key 当中,已经存在于集合的 member 元素将被忽略。
  • Smembers:返回集合 key 中的所有成员。
  • Sismember:判断 member 元素是否集合 key 的成员。
  • Srem:移除集合 key 中的一个或多个 member 元素,不存在的 member 元素会被忽略。 Go-tcp编程和Redis_第22张图片

4.3 Golang中操作Redis

(1)安装第三方开元Redis库:在GOPATH路径下执行安装指令(E:\goproject>go get github.com/garyburd/redigo/redis),安装成功后会在goproject文件中src文件中新增github.com文件。

  • Set/Get接口
package main

import (
	"fmt"

	"github.com/garyburd/redigo/redis" //引入Redis包
)

func main() {
	// 通过go 向Redis 写入数据和读取数据
	// 1.链接到Redis
	conn, err := redis.Dial("tcp", "127.0.0.1:6379") //此处的Redis.Conn是结构体
	if err != nil {
		fmt.Println("Redis.Dial err=", err)
		return
	}
	defer conn.Close() //关闭,非常重要

	// fmt.Printf("conn 的地址=%v\n", &conn)
	// fmt.Printf("conn 的格式=%T\n", conn) //*redis.Conn

	// 2.通过go 向Redis写入数据 string[key-val]
	_, err = conn.Do("Set", "name", "tom喵猫")
	if err != nil {
		fmt.Println("Set err=", err)
		return
	}

	// 2.通过go 向Redis读取数据 string[key-val]
	// r, err := conn.Do("Get", "name", "tom")
	// 因为返回的r是interface{}类型
	// 由于name对应的val是string,因此需要转换
	r, err := redis.String(conn.Do("Get", "name"))
	if err != nil {
		fmt.Println("Get err=", err)
		return
	}

	fmt.Printf("conn succ, name=%v\n", r)

}

  • Mset/Mget
  _,err:=conn,Do("Mset","name","全聚德","address","beijin")
  r,err:=redis.Strings(conn.Do("Mget","name","address"))
  for _,v:=range r{
  	fmt.Println(v)
  }

  • 给数据设置有效时间expire
    给name数据设置有效时间10s
  _,err:=conn,Do("expire","name",10)

或者使用setex

  _,err:=conn,Do("Setex","name",10,"老北京")

  • 操作Hash
    通过Golang对Redis操作Hash数据类型
    (1)对hash数据结构,file-val 逐一进行操作(写入和读取)
package main

import (
	"fmt"
	"github.com/garyburd/redigo/redis"
)

func main() {
	// 链接到Redis
	conn, err := redis.Dial("tcp", "127.0.0.1:6379")
	if err != nil {
		fmt.Println("redis.Dial err=", err)
		return
	}
	defer conn.Close() //记得关闭 十分重要

	// 向Redis中写入数据
	_, err = conn.Do("Hset", "user1", "name", "john")
	if err != nil {
		fmt.Println("hset err=", err)
		return
	}
	_, err = conn.Do("Hset", "user1", "age", 18)
	if err != nil {
		fmt.Println("hset err=", err)
		return
	}

	// 从Redis中取出数据
	r1, err := redis.String(conn.Do("Hget", "user1", "name"))
	if err != nil {
		fmt.Println("redis hget err=", err)
		return
	}
	r2, err := redis.Int(conn.Do("Hget", "user1", "age"))
	if err != nil {
		fmt.Println("redis hget err=", err)
		return
	}
	fmt.Printf("redis 中 hash信息name=%v,age=%v\n", r1, r2)
}

(2)对hash数据结构,file-val 批量进行操作(写入和读取)

package main

import (
	"fmt"

	"github.com/garyburd/redigo/redis"
)

func main() {
	conn, err := redis.Dial("tcp", "127.0.0.1:6379")
	if err != nil {
		fmt.Println("redis.Dail err=", err)
		return
	}
	defer conn.Close()

	_, err = conn.Do("hmset", "user2", "name", "Alex", "age", 25, "address", "北京")
	if err != nil {
		fmt.Println("hmset err=", err)
		return
	}
	//多个key-val转义需使用Strings
	r1, err := redis.Strings(conn.Do("hmget", "user2", "name", "age"))
	if err != nil {
		fmt.Println("hmget err", err)
	}
	r2, err := redis.Strings(conn.Do("hgetall", "user2"))
	if err != nil {
		fmt.Println("hmget err", err)
	}

	fmt.Println(r1) //[Alex 25]
	fmt.Println(r2) //[name Alex age 25 address 北京]

	// 取出r1/r2 []string 切片中的信息
	for i, v := range r1 {
		fmt.Printf("r[%d]=%s\n", i, v)
	}

	for _, v := range r2 {
		fmt.Printf("%v\n", v)
	}
}

练习1:

package main

import (
	"fmt"
	"strconv"

	"github.com/garyburd/redigo/redis"
)

//要求
// 1.Monster信息[name,age,skill]
// 2.通过终端输入三个monster的信息,使用golang操作Redis,存放到Redis中
// 3.编程,遍历出所有的monster信息,并显示在终端
func main() {
	conn, err := redis.Dial("tcp", "127.0.0.1:6379")

	if err != nil {
		fmt.Println("redis.Dial err", err)
		return
	}
	defer conn.Close()

	for i := 0; i < 3; i++ {
		monster := "monster" + strconv.FormatInt(int64(i), 10)
		name := ""
		age := 0
		skill := ""

		fmt.Println("输入姓名,年龄,技能:")
		fmt.Scanln(&name, &age, &skill)

		_, err = conn.Do("hmset", monster, "name", name, "age", age, "skil", skill)
		if err != nil {
			fmt.Println("hmset err", err)
			return
		}

		r, err := redis.Strings(conn.Do("hgetall", monster))
		if err != nil {
			fmt.Println("hgetall err", err)
			return
		}
		for _, v := range r {
			fmt.Println(v)
		}
	}
}


  • 操作List
    核心代码:
  _,err:=conn,Do("Lpush","mylist","no.1:宋江","no.2:晁盖","no.3:武松"18)
  r,err:=redis.Strin(conn.Do("rpop","mylist"))

练习2:

package main

import (
	"fmt"
	"math/rand"
	"strconv"
	"time"

	"github.com/garyburd/redigo/redis"
)

// 要求:
// 1.记录用户浏览商品信息,比如保存商品名
// 2.编写一个函数,可以取出某个用户最近浏览的10个商品名
// 3.提示考虑使用list数据类型

func visitList(c redis.Conn, s string) {
	_, err := c.Do("Lpush", "visitList", s)
	if err != nil {
		fmt.Println("Lpush err=", err)
		return
	}
	_, err = c.Do("Ltrim", "visitList", 0, 9)
	if err != nil {
		fmt.Println("Ltrim err=", err)
		return
	}

	// 显示列表中的信息
	r, err := redis.Strings(c.Do("Lrange", "visitList", 0, 9))
	if err != nil {
		fmt.Println("Lrange err=", err)
		return
	}
	fmt.Println("----历史信息----")
	for _, v := range r {
		fmt.Printf("%v\t", v)
	}
	fmt.Println("")

}

func main() {
	// 商品信息,商品名
	var arr []string
	rand.Seed(time.Now().UnixNano())

	conn, err := redis.Dial("tcp", "127.0.0.1:6379")
	if err != nil {
		fmt.Println("redis.dial err=", err)
		return
	}
	defer conn.Close()

	for i := 0; i < 15; i++ {
		arr = append(arr, "商品"+strconv.FormatInt(int64(rand.Intn(100)), 10))

		visitList(conn, arr[i])
		// 休眠五秒,累进一次
		time.Sleep(time.Second * 3)

	}

}

Redis链接池
说明:通过Golang对Redis操作,还可以通过Redis链接池,流程如下:
1)事先初始化一定数量的链接,放入到链接池
2)当Go需要操作Redis时,直接从Redis链接池取出链接即可
3)这样可以节省临时创建/获取Redis链接的时间,从而提高效率。
4)示意图
Go-tcp编程和Redis_第23张图片
核心代码:

var pool *redis.Pool
pool = &redis.Pool{ //结构体
	MaxIdle :8 //最大空闲链接数
	MaxActive : 0 //表示在给定时间下和数据库链接的最大链接数,0表述没有限制 
	IdleTimeout:100//最大空闲时间
	Dial:func()(redis.Conn,err){
		return redis.Dial("tcp","localhost:6379")	
	}
}
c:=pool.Get() //从链接池中取出一个链接
pool.Close()//关闭链接池,一旦链接池关闭,就不能再从链接池中取出链接

代码演示:

package main

import (
	"fmt"

	"github.com/garyburd/redigo/redis"
)

//定义一个全局变量 pool 是一个结构体指针
var pool *redis.Pool

//当程序启动时,初始化链接池
func init() {
	pool = &redis.Pool{
		MaxIdle:     8,   //最大空闲链接数
		MaxActive:   0,   //表示和数据库的最大链接数,0表示没有限制
		IdleTimeout: 100, //最大空闲时间
		Dial: func() (redis.Conn, error) { //初始化链接的代码,确定链接ip
			return redis.Dial("tcp", "localhost:6379")
		},
	}
}
func main() {
	// 先从pool取出一个链接
	conn := pool.Get()
	defer conn.Close() // 及时关闭链接不能忘

	//链接加入内容
	_, err := conn.Do("set", "name", "Leborn-James")
	if err != nil {
		fmt.Println("set err=", err)
		return
	}

	// 取出链接中的内容
	r, err := redis.String(conn.Do("get", "name"))
	if err != nil {
		fmt.Println("get err=", err)
		return
	}
	fmt.Println("r=", r)

	//若链接池关闭了,则无法从进行快速获取链接操作
	pool.Close()

	conn1 := pool.Get()
	defer conn1.Close()
	_, err = conn1.Do("set", "name1", "James-Harden")
	if err != nil {
		fmt.Println("set err=", err)
		return
	} //取不出链接,报错 链接池关闭  get on closed pool

	// 取出链接中的内容
	r1, err := redis.String(conn.Do("get", "name1"))
	if err != nil {
		fmt.Println("get err=", err)
		return
	}
	fmt.Println("r1=", r1)
}

你可能感兴趣的:(笔记,golang,tcp/ip,redis)