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)与硬件驱动对话
(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,结合任务管理器关闭不安全的端口
2.tcp socket编程
下图为Golang中 tcp socket编程中的客户端和服务器的网络分布。(tcp链接是长链接)
2.1 tcp socket编程快速入门
服务端的处理流程 1)监听端口 2)接收客户创建的tcp链接,建立客户端和服务器端的链接 3)创建goroutine,处理该链接的请求(通常客户端会通过链接发送请求包)
客户端的处理数据 1)建立与服务器间的链接 2)发送请求数据,接收服务器端发送的结果数据 3)关闭链接
远程测试 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()
for {
buf := make([]byte, 1024)
fmt.Printf("server are waiting for the response of client%s\t", conn.RemoteAddr().String())
n, err := conn.Read(buf)
if err != nil {
if err == io.EOF {
fmt.Println("server was closed")
} else {
fmt.Println("server 的Read err=", err)
}
return
}
fmt.Print(string(buf[:n]))
}
}
func main() {
fmt.Println("服务器开始监听...")
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 {
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)
}
}
- 客户端功能:
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
}
for {
reader := bufio.NewReader(os.Stdin)
str, err := reader.ReadString('\n')
if err != nil {
fmt.Println("reader readString err=", err)
return
}
if flag := strings.Trim(str, "\r\n"); flag == "exit" {
fmt.Println("客户端退出。。。")
fmt.Printf("客户端发送了%d字节的数据,并退出\n", total)
break
}
n, err := conn.Write([]byte(str))
if err != nil {
fmt.Println("conn.Write err=", err)
}
total += n
}
}
3.海量用户即时通讯系统
3.1项目开发流程
需求分析——>设计阶段——>编码实现——>测试阶段——>实施 需求分析:
- 用户注册
- 用户登录
- 显示在线用户列表
- 群聊(广播)
- 点对点聊天
- 离线留言
3.2界面设计
3.3 项目开发前技术准备
项目需要保存用户信息和消息数据,因此需要运用到数据库(Redis或MySQL)的知识,先了解在Golang中运用Redis。
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基本原理图
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命令将覆写旧值。 (2)mset【同时设置一个或多个key-val对】(如果存在已有的key,则覆写旧值 );mget【同时获取一个或多个val值】。 例如:同时设置两个工人的名字,并同时获得
**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,不论是从左或是从右推入数据,其元素间的指向都是从左到右的关系。 -
List中的元素全移除,对应的键也就消失了。
指令代码:
lpush 将一个或多个value插入到列表key的标头,遵循先入后出的方式。lrange 返回列表 key 中指定区间内的元素,区间以偏移量 start 和 stop 指定。(0表示表左边第一个元素,-1表示表右边第一个元素) 举例:存放多个地址信息,city 北京 天津 上海 rpush 将一个或多个值 value 插入到列表 key 的表尾(最右边)。 Lpop 移除并返回列表key的头元素。Rpop 移除并返回列表 key 的尾元素。 del 删除,del key 直接删除列表Lset 将列表 key 下标为 index 的元素的值设置为 value 。 Lindex :Lindex key index 返回列表 key 中,下标为 index 的元素。LLen :返回列表key的长度。
Set(集合):
- Redis的Set是string类型的无序集合。
- 底层是Hash Table数据结构,Set也是存放很多字符串元素,字符串元素是无序的,而且元素值不能重复。
用途:用于存放唯一形式/独一份的数据,例如电子邮件 指令代码:
Sadd :将一个或多个 member 元素加入到集合 key 当中,已经存在于集合的 member 元素将被忽略。Smembers :返回集合 key 中的所有成员。Sismember :判断 member 元素是否集合 key 的成员。Srem :移除集合 key 中的一个或多个 member 元素,不存在的 member 元素会被忽略。
4.3 Golang中操作Redis
(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"
)
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()
_, err = conn.Do("Set", "name", "tom喵猫")
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.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 给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() {
conn, err := redis.Dial("tcp", "127.0.0.1:6379")
if err != nil {
fmt.Println("redis.Dial err=", err)
return
}
defer conn.Close()
_, 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
}
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
}
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)
fmt.Println(r2)
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"
)
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"
)
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"
)
var pool *redis.Pool
func init() {
pool = &redis.Pool{
MaxIdle: 8,
MaxActive: 0,
IdleTimeout: 100,
Dial: func() (redis.Conn, error) {
return redis.Dial("tcp", "localhost:6379")
},
}
}
func main() {
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
}
r1, err := redis.String(conn.Do("get", "name1"))
if err != nil {
fmt.Println("get err=", err)
return
}
fmt.Println("r1=", r1)
}
|