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)与硬件驱动对话
(3)IP地址
概述:每个Internet上的主机和路由器都有一个ip地址,它包括网络号和主机号,IP地址有ipv4(32位)和ipv6(128位)。可以通过ipconfig来查看。
(4)端口(port)介绍
非物理意义上的端口/连接处,而是特指tcp/ip协议中的端口,是逻辑意义上的端口。
如果把ip地址比作成一个房间,端口就是出入这间房的门。实际的房间最多只有几个门,但是一个ip地址的端口可以有65536(即256*256)个之多!端口是通过端口号来标识的,端口号只有整数,范围从0-65535。需注意不是所有的端口号都能在开发中使用,例如端口号为0的端口就很特殊。可以通过netstat -a查看占用端口号。
端口(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,结合任务管理器关闭不安全的端口
下图为Golang中 tcp socket编程中的客户端和服务器的网络分布。(tcp链接是长链接)
服务端的处理流程
1)监听端口
2)接收客户创建的tcp链接,建立客户端和服务器端的链接
3)创建goroutine,处理该链接的请求(通常客户端会通过链接发送请求包)
客户端的处理数据
1)建立与服务器间的链接
2)发送请求数据,接收服务器端发送的结果数据
3)关闭链接
远程测试
telnet
例如想测试百度的80端口是否在正常运行telnet www.baidu.com 80
退出使用control +]
再输入quit
查找自己的ip地址
使用ipconfig
代码的实现
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)
}
客户端的代码: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
}
}
需求分析——>设计阶段——>编码实现——>测试阶段——>实施
需求分析:
项目需要保存用户信息和消息数据,因此需要运用到数据库(Redis或MySQL)的知识,先了解在Golang中运用Redis。
1.Redis是NoSQL数据库,不是传统的关系型数据库。(官网http://www.redis.cn)
2.Redis全称为Remote Dictionary Server(远程字典服务器),Redis性能非常高,单机能够达到15w qps,通常适合做缓存,也可以持久化
3.是完全开源免费的,高性能的(key/value)分布式内存数据库,基于内存运行并支持持久化的NoSQL数据库,是最热门的NoSQL数据库之一,也称为数据结构服务器。
4.Redis基本原理图
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】
Redis支持的五大数据类型:String(字符串)、Hash(哈希)、List(列表)、Set(集合)、zset(sorted set 有序集合)。( 更多细节见http://redisdoc.com)
String(字符串):
举例:存放一个地址信息,key:address ; value: beijin
setex key seconds valus
。该命令类似于:set key value
+expire value seconds
。当key已经存在,SETE命令将覆写旧值。**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”
hgetall
获得所有hash表key中的所有域和值。
hgetall key
hdel
删除hash表中key的某个或多个域,如果域不存在将会被忽略。
hdel key field
**hmset
\ hmget
同时将多个field-val设置到hash表中\返回hash表中一个或多个指定域的值。
hlen
统计hash表中的字段。
hlen key
hexists
判断hash表中key是否有存在某个字段,存在时返回1,不存在返回0。
hexists key field
List(列表):
列表是简单的字符串列表,按照插入顺序排序。可以添加一个元素到表的头部(左边)或者尾部(右边)
List的本质是个链表,List的元素是有序的,元素的值是可以重复的。
List,不论是从左或是从右推入数据,其元素间的指向都是从左到右的关系。
指令代码:
lpush
将一个或多个value插入到列表key的标头,遵循先入后出的方式。lrange
返回列表 key 中指定区间内的元素,区间以偏移量 start 和 stop 指定。(0表示表左边第一个元素,-1表示表右边第一个元素)rpush
将一个或多个值 value 插入到列表 key 的表尾(最右边)。Lpop
移除并返回列表key的头元素。Rpop
移除并返回列表 key 的尾元素。del
删除,del key
直接删除列表Lset
将列表 key 下标为 index 的元素的值设置为 value 。Lindex
:Lindex key index
返回列表 key 中,下标为 index 的元素。LLen
:返回列表key的长度。Set(集合):
用途:用于存放唯一形式/独一份的数据,例如电子邮件
指令代码:
Sadd
:将一个或多个 member 元素加入到集合 key 当中,已经存在于集合的 member 元素将被忽略。Smembers
:返回集合 key 中的所有成员。Sismember
:判断 member 元素是否集合 key 的成员。Srem
:移除集合 key 中的一个或多个 member 元素,不存在的 member 元素会被忽略。 (1)安装第三方开元Redis库:在GOPATH路径下执行安装指令(E:\goproject>go get github.com/garyburd/redigo/redis),安装成功后会在goproject文件中src文件中新增github.com文件。
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)
}
_,err:=conn,Do("Mset","name","全聚德","address","beijin")
r,err:=redis.Strings(conn.Do("Mget","name","address"))
for _,v:=range r{
fmt.Println(v)
}
expire
_,err:=conn,Do("expire","name",10)
或者使用setex
_,err:=conn,Do("Setex","name",10,"老北京")
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)
}
}
}
_,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)示意图
核心代码:
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)
}