Go第 18 章 :tcp编程
18.1 看两个实际应用
QQ,迅雷,百度网盘客户端.
新浪网站,京东商城,淘宝…
18.2 网络编程基本介绍
Golang 的主要设计目标之一就是面向大规模后端服务程序,网络通信这块是服务端 程序必不可少
也是至关重要的一部分。
网络编程有两种:
1) TCP socket 编程,是网络编程的主流。之所以叫 Tcp socket 编程,是因为底层是基于 Tcp/ip 协 议的. 比如: QQ 聊天 [示意图]
2) b/s 结构的 http 编程,我们使用浏览器去访问服务器时,使用的就是 http 协议,而 http 底层依 旧是用 tcp socket 实现的。[示意图] 比如: 京东商城 【这属于 go web 开发范畴 】
18.2.1 网线,网卡,无线网卡
计算机间要相互通讯,必须要求网线,网卡,或者是无线网卡.
18.2.2 协议(tcp/ip)
TCP/IP(Transmission Control Protocol/Internet Protocol)的简写,中文译名为传输控制协议/因特网互 联协议,又叫网络通讯协议,这个协议是 Internet 最基本的协议、Internet 国际互联网络的基础,简单 地说,就是由网络层的 IP 协议和传输层的 TCP协议组成的。
18.2.3 OSI 与 Tcp/ip 参考模型 (推荐 tcp/ip 协议 3 卷)
18.2.4 ip 地址
概述:每个 internet 上的主机和路由器都有一个 ip地址,它包括网络号和主机号,ip地址有 ipv4(32
位)或者 ipv6(128 位). 可以通过 ipconfig 来查看
18.2.5 端口(port)-介绍
我们这里所指的端口不是指物理意义上的端口,而是特指 TCP/IP 协议中的端口,是
逻辑意义上的端口。
如果把 IP 地址比作一间房子,端口就是出入这间房子的门。真正的房子只有几个 门,但是一个 IP 地址的端口 可以有 65536(即:256×256)个之多!端口是通过端 口号来标记的,端口号只有整数,范围是从 0 到 65535(256×256-1)
18.2.6 端口(port)-分类
18.2.7 端口(port)-使用注意
- 在计算机(尤其是做服务器)要尽可能的少开端口
- 一个端口只能被一个程序监听
- 如果使用 netstat –an 可以查看本机有哪些端口在监听
- 可以使用 netstat –anb 来查看监听端口的 pid,在结合任务管理器关闭不安全的端口
18.3 tcp socket 编程的客户端和服务器端
18.4 tcp socket 编程的快速入门
18.4.1 服务端的处理流程
- 监听端口 8888
- 接收客户端的 tcp 链接,建立客户端和服务器端的链接.
- 创建 goroutine,处理该链接的请求(通常客户端会通过链接发送请求包)
18.4.2 客户端的处理流程
- 建立与服务端的链接
- 发送请求数据[终端],接收服务器端返回的结果数据
- 关闭链接
18.4.3 简单的程序示意图
18.4.4 代码的实现
package main
import (
"fmt"
"net"
)
func process(conn net.Conn) {
//这里我们循环的接收客户端发送的数据
defer conn.Close() //关闭conn
for {
//创建一个新的切片
buf := make([]byte, 1024)
//conn.Read(buf)
//1、等待客户端通过conn发送信息
//2、如果客户端没有write【发送】,那么协程就阻塞在这里
fmt.Printf("服务器在等待客户端%d发送信息 \n", conn.RemoteAddr().String())
n, err := conn.Read(buf) //从conn读取
if err != nil {
fmt.Println("客户端退出err=", err)
return //!!!
}
//3. 显示客户端发送的内容到服务器的终端
fmt.Print(string(buf[:n]))
}
}
func main() {
fmt.Println("服务器开始监听...")
//net.Listen("tcp","0.0.0.0:8888")
//1. tcp 表示使用网络协议是 tcp
//2. 0.0.0.0:8888 表示在本地监听 8888 端口
listen, err := net.Listen("tcp", ":8888")
if err != nil {
fmt.Println("listen err= ", err)
return
}
defer listen.Close()
for { //循环等待客户端来连接我
fmt.Println("等待客户端连接。。。")
conn, err := listen.Accept()
if err != nil {
fmt.Println("conn err=", err)
} else {
fmt.Printf("accept() success con=%v 客户端ip=%v\n", conn, conn.RemoteAddr().String())
}
//这里准备其一个协程,为客户端服务
go process(conn)
}
//fmt.Printf("listen suc=%v\n", listen)
}
客户端功能:
- 编写一个客户端端程序,能链接到 服务器端的 8888 端口
-
- 客户端可以发送单行数据,然后就退出
-
- 能通过终端输入数据(输入一行发送一行), 并发送给服务器端 []
-
- 在终端输入 exit,表示退出程序.
-
- 代码:
package main
import (
"bufio"
"fmt"
"net"
"os"
)
func main(){
conn,err:=net.Dial("tcp",":8888")
if err!=nil{
fmt.Println("Dial err = ",err)
return
}
//功能一:客户端可以发送单行数据,然后就退出
reader:=bufio.NewReader(os.Stdin)//os.Stdin 代表标准输入[终端]
//从终端读取一行用户输入,并准备发送给服务器
line,err:=reader.ReadString('\n')
if err!=nil{
fmt.Println("ReadString err = ",err)
}
//再将 line 发送给 服务器
n,err:=conn.Write([]byte(line))
if err!=nil{
fmt.Println("Write err = ",err)
}
fmt.Printf("客户端发送了 %d 字节的数据,并退出", n)
}
对 client.go 做了改进:
package main
import (
"bufio"
"fmt"
"net"
"os"
"strings"
)
func main() {
conn, err := net.Dial("tcp", ":8888")
if err != nil {
fmt.Println("Dial err = ", err)
return
}
//功能一:客户端可以发送单行数据,然后就退出
reader := bufio.NewReader(os.Stdin) //os.Stdin 代表标准输入[终端]
for {
//从终端读取一行用户输入,并准备发送给服务器
line, err := reader.ReadString('\n')
if err != nil {
fmt.Println("ReadString err = ", err)
}
line = strings.Trim(line, " \r\n")
if line == "exit" {
fmt.Println("客户端退出..")
break
}
//再将 line 发送给 服务器
_, err = conn.Write([]byte(line + "\n"))
if err != nil {
fmt.Println("Write err = ", err)
}
//fmt.Printf("客户端发送了 %d 字节的数据,并退出", n)
}
}
18.5 经典项目-海量用户即时通讯系统
18.5.1 项目开发流程
需求分析–> 设计阶段—> 编码实现 --> 测试阶段–>实施
18.5.2 需求分析
- 用户注册
- 用户登录
- 显示在线用户列表
- 群聊(广播)
- 点对点聊天
- 离线留言
18.5.3 界面设计
18.5.4 项目开发前技术准备
项目要保存用户信息和消息数据,因此我们需要学习数据库(Redis 或者 Mysql) , 这里我们选择 Redis , 所以先给同学们讲解如何在 Golang 中使用 Redis.
18.5.5 实现功能-显示客户端登录菜单
功能:能够正确的显示客户端的菜单。
界面:
client/main.go
client/login.go
18.5.6 实现功能-完成用户登录
要求:先完成指定用户的验证,用户 id=100, 密码 pwd=123456 可以登录,其它用户不能登录
这里需要先说明一个 Message 的组成(示意图),并发送一个 Message 的流程
1.完成客户端可以发送消息长度,服务器端可以正常收到该长度值
分析思路
(1) 先确定消息 Message 的格式和结构
(2) 然后根据上图的分析完成代码
(3) 示意图
代码实现:
server/main.go
package main
import (
"fmt"
"net"
)
//处理和客户端的通讯
func process(conn net.Conn){
//这里需要延时关闭conn,否则后边出现非常奇怪的现象,代码多了不好调
defer conn.Close()
//循环的读取客户端发送的信息
for{
buf := make([]byte,8096)
fmt.Println()
fmt.Println("开始读取客户端发送的数据......")
n,err := conn.Read(buf[:4])
if n!=4 || err != nil {
fmt.Println("conn.Read,err=", err)
return //或者干别的事情,如果不设置就会阻塞在这个地方
}
fmt.Printf("读到的长度为=%d, 读到的buf=%v\n",n,buf[:4])
}
}
func main() {
//提示信息
fmt.Println("服务器再8889端口监听......")
Listen, err := net.Listen("tcp", "127.0.0.1:8889")
defer Listen.Close()
if err != nil {
fmt.Println("net.listen err=", err)
return
}
//一旦监听成功,就等待客户端来链接服务器
for{
fmt.Println("等待客户端来链接服务器......")
conn,err := Listen.Accept()
if err != nil {
fmt.Println("conn.Accept err=", err)
}
//一旦链接成功,则启动一个协程和客户端保持数据的通讯......
go process(conn)
}
}
common/message/message.go
package message
const(
LoginMesType = "LoginMes"
LoginResMesType = "LoginResMes"
)
//因为在传递序列化的时候需要变量是小写的,因此需要打一个小写的tag
type Message struct{
Type string `json:"type"`//消息类型
Data string `json:"data"`//消息内容
}
//定义两个消息。。。后面需要再增加
type LoginMes struct{
UserId int `json:"userId"`//用户id
UserPwd string `json:"userPwd"` //用户密码
UserName string `json:"userName"`//用户名
}
type LoginResMes struct{
Code int `json:"code"`//返回状态码 500 表示该用户未注册 200表示登录成功 300表示网络不通
Error string `json:"error"`//错误描述 若没有错误则返回nil
}
client/main.go 和前面的代码一样,没有修改
client/login.go
package main
import (
"encoding/binary"
"encoding/json"
"fmt"
"go_code/go_code/chapter18/chatroom/common/message"
"net"
)
//写一个函数,完成一个登录校验
func Login(UserId int, UserPwd string) (err error){
//下一步就要开始定协议(大事)
//fmt.Printf(" userId =%d userPw = %s\n", UserId,UserPwd)
//return nil
//1、链接服务器
conn,err := net.Dial("tcp","localhost:8889")
if err != nil {
fmt.Println("net.Dial err=", err)
return
}
//延时关闭
defer conn.Close()
//2、准备通过conn发送消息给服务器
var mes message.Message
mes.Type = message.LoginMesType
//3、创建一个LoginMesTypMes结构体
var loginMes message.LoginMes
loginMes.UserId = UserId
loginMes.UserPwd = UserPwd
//4、将loginMes序列化
data,err := json.Marshal(loginMes)
if err != nil {
fmt.Println("loginMes json.Marshal err=", err)
return
}
//5、把data赋给来mes.Data字段
mes.Data = string(data)
//6、将mes进行序列化
data,err = json.Marshal(mes)
if err != nil {
fmt.Println(" mes json.Marshal err=", err)
return
}
//7、到这个时候,data就是我们要发送的消息
//7.1先把data的长度发送给服务器,告诉它要接收多少个字节的内容
//先获取到data的长度-》转成一个表示长度的byte切片
// 错误写法: conn.Write(len(data))
//正确写法:将长度转成一个bytes
//需要用到binarry包里的func (bigEndian) PutUint32(b []byte, v uint32){}
var pkgLen uint32
pkgLen = uint32(len(data))
var buf [4]byte //定义一个数组
binary.BigEndian.PutUint32(buf[0:4],pkgLen)//BigEndian是实例化一个对象,从而可以调用它的方法PutUint32
//发送长度
n,err := conn.Write(buf[:4])
if n!= 4|| err!=nil{
fmt.Println("conn.Write(bytes)fail,err=",err)
return
}
fmt.Printf("客户端,发送数据的长度ok~~长度为%d,内容为:v\n",len(data),string(data))
return
}
2.完成客户端可以发送消息本身,服务器端可以正常接收到消息,并根据客户端发送的消息(LoginMes), 判断用户的合法性,并返回相应的 LoginResMes
思路分析:
(1) 让客户端发送消息本身
(2) 服务器端接受到消息, 然后反序列化成对应的消息结构体.
(3) 服务器端根据反序列化成对应的消息, 判断是否登录用户是合法, 返回 LoginResMes
(4) 客户端解析返回的 LoginResMes,显示对应界面
(5) 这里我们需要做函数的封装
代码实现:
client/login.go 做了修改
package main
import (
"encoding/binary"
"encoding/json"
"fmt"
"go_code/go_code/chapter18/chatroom/common/message"
"net"
"time"
)
//写一个函数,完成一个登录校验
func Login(UserId int, UserPwd string) (err error){
//下一步就要开始定协议(大事)
//fmt.Printf(" userId =%d userPw = %s\n", UserId,UserPwd)
//return nil
//1、链接服务器
conn,err := net.Dial("tcp","localhost:8889")
if err != nil {
fmt.Println("net.Dial err=", err)
return
}
//延时关闭
defer conn.Close()
//2、准备通过conn发送消息给服务器
var mes message.Message
mes.Type = message.LoginMesType
//3、创建一个LoginMesTypMes结构体
var loginMes message.LoginMes
loginMes.UserId = UserId
loginMes.UserPwd = UserPwd
//4、将loginMes序列化
data,err := json.Marshal(loginMes)
if err != nil {
fmt.Println("loginMes json.Marshal err=", err)
return
}
//5、把data赋给来mes.Data字段
mes.Data = string(data)
//6、将mes进行序列化
data,err = json.Marshal(mes)
if err != nil {
fmt.Println(" mes json.Marshal err=", err)
return
}
//7、到这个时候,data就是我们要发送的消息
//7.1先把data的长度发送给服务器,告诉它要接收多少个字节的内容
//先获取到data的长度-》转成一个表示长度的byte切片
// 错误写法: conn.Write(len(data))
//正确写法:将长度转成一个bytes
//需要用到binarry包里的func (bigEndian) PutUint32(b []byte, v uint32){}
var pkgLen uint32
pkgLen = uint32(len(data))
var buf [4]byte //定义一个数组
binary.BigEndian.PutUint32(buf[0:4],pkgLen)//BigEndian是实例化一个对象,从而可以调用它的方法PutUint32
//发送长度
n,err := conn.Write(buf[:4])
if n!= 4|| err!=nil{
fmt.Println("conn.Write(bytes)fail,err=",err)
return
}
//fmt.Printf("客户端,发送数据的长度ok~~长度为%d,内容为:v\n",len(data),string(data))
//发送消息本身
_,err = conn.Write(data)
if err!=nil{
fmt.Println("conn.Write(data)fail,err=",err)
return
}
//休眠20
time.Sleep(20 * time.Second)
fmt.Println("休眠了20s.......")
// 这里还需要处理服务器端返回的消息
return
}
server/main.go 修改
package main
import (
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"go_code/go_code/chapter18/chatroom/common/message"
"io"
"net"
)
//在函数头的返回值中声明了 mes 可在函数体中不作声明
func readpkg(conn net.Conn)(mes message.Message,err error){
buf := make([]byte,8096)
fmt.Println("开始读取客户端发送的数据......")
//conn.Read 在conn没有被关闭的情况下,才会阻塞
//如果客户端关闭了conn,则不会阻塞了
_,err = conn.Read(buf[:4])
if err != nil {
fmt.Println("conn.Read(buf[:pkgLen]) err,err=",err) //return或者干别的事情,如果不设置就会阻塞在这个地方
return
}
fmt.Printf("读到的buf=%v\n",buf[:4])
//根据读到的buf长度,转成一个 uint32类型
var pkgLen uint32
pkgLen = binary.BigEndian.Uint32(buf[:4])
//根据pkgLen 读取消息内容
n,err := conn.Read(buf[:pkgLen])
if n!=int(pkgLen) {
err = errors.New("read pkg body error")
//fmt.Println("conn.Read(buf[:pkgLen]) err,err=",err)
return
}else if err != nil{
fmt.Println("conn.Read(buf[:pkgLen]) err ,err=",err)
return
}
//把pkgLen反序列化成->message.Message类型 json.Unmarshal(buf[:pkgLen],mes) mes必须加&。
//技术就是一层窗户纸,捅出来就不神秘了
err = json.Unmarshal(buf[:pkgLen],&mes)
if err != nil {
fmt.Println("反序列化失败,err=", err)
return
}
return
}
//处理和客户端的通讯
func process(conn net.Conn){
//这里需要延时关闭conn,否则后边出现非常奇怪的现象,代码多了不好调
defer conn.Close()
//循环的读取客户端发送的信息
for{
//这里将读取数据包,直接封装成一个函数readpkg message,Err
mes,err:=readpkg(conn)
if err != nil {
if err ==io.EOF{
fmt.Println("客户端退出,服务器端也正常退出")
return
}else{
fmt.Println("readpkg err=", err)
}
}
fmt.Println("mes=",mes)
}
}
func main() {
//提示信息
fmt.Println("服务器再8889端口监听......")
Listen, err := net.Listen("tcp", "127.0.0.1:8889")
defer Listen.Close()
if err != nil {
fmt.Println("net.listen err=", err)
return
}
//一旦监听成功,就等待客户端来链接服务器
for{
fmt.Println("等待客户端来链接服务器......")
conn,err := Listen.Accept()
if err != nil {
fmt.Println("conn.Accept err=", err)
}
//一旦链接成功,则启动一个协程和客户端保持数据的通讯......
go process(conn)
}
}
将读取包的任务封装到了一个函数中.readPkg()
能够完成登录,并提示相应信息
server/main.go 修改
client/utils.go 新增
client/login.go 增加代码
程序结构的改进, 前面的程序虽然完成了功能,但是没有结构,系统的可读性、扩展性和维护性都不好,因此需要对程序的结构进行改进。
-
- 先改进服务端, 先画出程序的框架图[思路],再写代码.
- 先改进服务端, 先画出程序的框架图[思路],再写代码.
-
- 步骤
[1] . 先把分析出来的文件,创建好,然后放到相应的文件夹[包]
[2] 现在根据各个文件,完成的任务不同,将 main.go 的代码剥离到对应的文件中即可。
[3] 先修改了 utils/utils.go
[4] 修改了 process2/userProcess.go
[5] 修改了 main/processor.go
[6] 修改 main/main.go
- 步骤
修改客户端, 先画出程序的框架图[思路],再写代码
[1] 步骤 1-画出示意图
[2] 先把各个文件放到对应的文件夹[包]
login.go
package main
import (
"encoding/binary"
"encoding/json"
"fmt"
"go_code/go_code/chatroom/client/utils"
"go_code/go_code/chatroom/common/message"
"net"
)
//写一个函数,完成一个登录校验
func Login(UserId int, UserPwd string) (err error){
//下一步就要开始定协议(大事)
//fmt.Printf(" userId =%d userPw = %s\n", UserId,UserPwd)
//return nil
//1、链接服务器
conn,err := net.Dial("tcp","localhost:8889")
if err != nil {
fmt.Println("net.Dial err=", err)
return
}
//延时关闭
defer conn.Close()
//2、准备通过conn发送消息给服务器
var mes message.Message
mes.Type = message.LoginMesType
//3、创建一个LoginMesTypMes结构体
var loginMes message.LoginMes
loginMes.UserId = UserId
loginMes.UserPwd = UserPwd
//4、将loginMes序列化
data,err := json.Marshal(loginMes)
if err != nil {
fmt.Println("loginMes json.Marshal err=", err)
return
}
//5、把data赋给来mes.Data字段
mes.Data = string(data)
//6、将mes进行序列化
data,err = json.Marshal(mes)
if err != nil {
fmt.Println(" mes json.Marshal err=", err)
return
}
//7、到这个时候,data就是我们要发送的消息
//7.1先把data的长度发送给服务器,告诉它要接收多少个字节的内容
//先获取到data的长度-》转成一个表示长度的byte切片
// 错误写法: conn.Write(len(data))
//正确写法:将长度转成一个bytes
//需要用到binarry包里的func (bigEndian) PutUint32(b []byte, v uint32){}
var pkgLen uint32
pkgLen = uint32(len(data))
var buf [4]byte //定义一个数组
binary.BigEndian.PutUint32(buf[0:4],pkgLen)//BigEndian是实例化一个对象,从而可以调用它的方法PutUint32
//发送长度
n,err := conn.Write(buf[:4])
if n!= 4|| err!=nil{
fmt.Println("conn.Write(bytes)fail,err=",err)
return
}
//fmt.Printf("客户端,发送数据的长度ok~~长度为%d,内容为:v\n",len(data),string(data))
//发送消息本身
_,err = conn.Write(data)
if err!=nil{
fmt.Println("conn.Write(data)fail,err=",err)
return
}
//休眠20
//time.Sleep(20 * time.Second)
//fmt.Println("休眠了20s.......")
//处理服务器返回的消息
mes,err = utils.readpkg(conn) //mes 就是
if err != nil {
fmt.Println("如果readpkg(conn) is error err=", err)
return
}
//将mes的Data部分反序列化成loginResMes
var loginResMes message.LoginResMes
err =json.Unmarshal([]byte(mes.Data),&loginResMes)
if err != nil {
fmt.Println("Unmarshal([]byte(mes.Data),&loginResMes) is error,err=", err)
return
}
if loginResMes.Code == 200{
fmt.Println("登录成功")
}else if loginResMes.Code == 500{
fmt.Println(loginResMes.Error)
}
return
}
[3] 将 server/utils.go 拷贝到 client/utils/utils.go
[4] 创建了 server/process/userProcess.go
说明:该文件就是在原来的 login.go 做了一个改进,即封装到 UserProcess 结构体
[5] 创建了 server/process/server.go
[6] server/main/main.go 修改
在 Redis 手动添加测试用户,并画图+说明注意. (后面通过程序注册用户)
MVC main.go是现实就代表V processor.go处理器代表C model就代表M,但是在项目复杂的时候还有一个service层,本项目比较简单所用有userDao.go就够了
如输入的用户名密码在 Redis 中存在则登录,否则退出系统,并给出相应的提示信息:
- 用户不存在,你也可以重新注册,再登录
- 你密码不正确。。
代码实现:
[1] 编写 model/user.go
[2] 编写 model/error.go
[3] 编写 model/userDao.go
package model
import (
"encoding/json"
"fmt"
"github.com/garyburd/redigo/redis"
)
//我们在服务器启动后,就初始化一个userDao实例,
//把它做成一个全局的变量,在需要和redis操作时就直接使用即可
var (
MyUserDao *UserDao
)
//定义一个UserDao 结构体体
//完成对User 结构体的各种操作
type UserDao struct{
pool *redis.Pool
}
//使用工厂模式,创建一个UserDao的实例
func NewUserDao(pool *redis.Pool)(userDao *UserDao){
userDao = &UserDao{
pool:pool,
}
return
}
//思考一下在UserDao中应该提供哪些方法
//1、根据一个用户ID,返回一个User实例+err
func (dao *UserDao) getUserById(conn redis.Conn,id int)(user *User,err error){
//通过给定的id去redis查询用户
res,err := redis.String(conn.Do("HGet","users",id))
if err != nil {
if err == redis.ErrNil{ //表示在users 哈希中没有找到对应的ID
err = ERROR_USER_NOTEXITS
}
fmt.Println("Do.HGet is error,err=", err)
return
}
user = &User{}//struct实例是值类型,不需要使用&User{}获取空间
//这里需要把res 反序列化成一个User实例
err = json.Unmarshal([]byte(res),user)
if err != nil {
fmt.Println("json.Unmarshal([]byte(res),user) is error,err=", err)
return
}
return
}
//完成登录校验
//1、Login 完成对用户的校验
//2、如果用户的id和pwd都正确,则返回一个user实例
//3、如果用户的id或者密码有错误,则返回对应的错误信息
func (this *UserDao) Login(userId int,userPwd string)(user *User,err error){
//先从UserDao的连接池中取出一根连接
conn := this.pool.Get()
defer conn.Close()
user,err = this.getUserById(conn,userId)
if err != nil {
return
}
//这时证明这个用户是获取到的,但是密码不一定正确
if user.UserPwd != userPwd {
err = ERROR_USER_PWD
return
}
return
}
[4] main/redis.go
[5] main/main.go
[6] 在 process/userProcess.go 使用到 redis 验证的功能
18.5.7 实现功能-完成注册用户
- 完成注册功能,将用户信息录入到 Redis 中
- 思路分析,并完成代码
- 思路分析的示意图
实现功能-完成注册用户
[1] common/message/user.go
[2] common/message/message.go
[3] client/process/userProcess.go
func (this *UserProcess) Register(userId int,
userPwd string, userName string) (err error) {
//1. 链接到服务器
conn, err := net.Dial("tcp", "localhost:8889")
if err != nil {
fmt.Println("net.Dial err=", err)
return
} //延时关闭 defer conn.Close()
//2. 准备通过 conn 发送消息给服务
var mes message.Message
mes.Type = message.RegisterMesType
//3. 创建一个 LoginMes 结构体
var registerMes message.RegisterMes
registerMes.User.UserId = userId
registerMes.User.UserPwd = userPwd
registerMes.User.UserName = userName
//4.将 registerMes 序列化
data, err := json.Marshal(registerMes)
if err != nil {
fmt.Println("json.Marshal err=", err)
return
}
// 5. 把 data 赋给 mes.Data 字段
mes.Data = string(data)
// 6. 将 mes 进行序列化化 d
ata, err = json.Marshal(mes)
if err != nil {
fmt.Println("json.Marshal err=", err)
return
}
//创建一个 Transfer 实例
tf := &utils.Transfer{
Conn : conn,
}
//发送 data 给服务器端
err = tf.WritePkg(data)
if err != nil {
fmt.Println("注册发送信息错误 err=", err)
}
mes, err = tf.ReadPkg() // mes 就是 RegisterResMes
if err != nil {
fmt.Println("readPkg(conn) err=", err)
return
}
//将 mes 的 Data 部分反序列化成 RegisterResMes
var registerResMes message.RegisterResMes
err = json.Unmarshal([]byte(mes.Data), ®isterResMes)
if registerResMes.Code == 200 {
fmt.Println("注册成功, 你重新登录一把")
os.Exit(0)
} else {
fmt.Println(registerResMes.Error)
os.Exit(0)
}
return
}
[4] 在 client/main/main.go 增加了代码
[5] 在 server/model/userDao.go 增加方法
[6] 在 server/process/userProcess.go 增加了方法,处理注册
package model
import (
"encoding/json"
"fmt"
"github.com/garyburd/redigo/redis"
"go_code/go_code/chatroom/common/message"
)
//我们在服务器启动后,就初始化一个userDao实例,
//把它做成全局的变量,在需要和redis操作时,就直接使用即可
var (
MyUserDao *UserDao
)
//定义一个UserDao 结构体体
//完成对User 结构体的各种操作.
type UserDao struct {
pool *redis.Pool
}
//使用工厂模式,创建一个UserDao实例
func NewUserDao(pool *redis.Pool) (userDao *UserDao) {
userDao = &UserDao{
pool: pool,
}
return
}
//思考一下在UserDao 应该提供哪些方法给我们
//1. 根据用户id 返回 一个User实例+err
func (this *UserDao) getUserById(conn redis.Conn, id int) (user *User, err error) {
//通过给定id 去 redis查询这个用户
res, err := redis.String(conn.Do("HGet", "users", id))
if err != nil {
//错误!
if err == redis.ErrNil { //表示在 users 哈希中,没有找到对应id
err = ERROR_USER_NOTEXITS
}
return
}
user = &User{}
//这里我们需要把res 反序列化成User实例
err = json.Unmarshal([]byte(res), user)
if err != nil {
fmt.Println("json.Unmarshal err=", err)
return
}
return
}
//完成登录的校验 Login
//1. Login 完成对用户的验证
//2. 如果用户的id和pwd都正确,则返回一个user实例
//3. 如果用户的id或pwd有错误,则返回对应的错误信息
func (this *UserDao) Login(userId int, userPwd string) (user *User, err error) {
//先从UserDao 的连接池中取出一根连接
conn := this.pool.Get()
defer conn.Close()
user, err = this.getUserById(conn, userId)
if err != nil {
return
}
//这时证明这个用户是获取到.
if user.UserPwd != userPwd {
err = ERROR_USER_PWD
return
}
return
}
func (this *UserDao) Register(user *message.User) (err error) {
//先从UserDao 的连接池中取出一根连接
conn := this.pool.Get()
defer conn.Close()
_, err = this.getUserById(conn, user.UserId)
if err == nil {
err = ERROR_USER_EXISTS
return
}
//这时,说明id在redis还没有,则可以完成注册
data, err := json.Marshal(user) //序列化
if err != nil {
return
}
//入库
_, err = conn.Do("HSet", "users", user.UserId, string(data))
if err != nil {
fmt.Println("保存注册用户错误 err=", err)
return
}
return
}
[7] server/main/processor.go 调用了
18.5.8 实现功能-完成登录时能返回当前在线用户
用户登录后,可以得到当前在线用户列表思路分析、示意图、代码实现
思路分析:
代码实现:
[1] 编写了 server/process/userMgr.go
package process2
import "fmt"
//因为UserMgr 实例在服务器端有且只有一个
//因为在很多的地方,都会使用到,因此,我们
//将其定义为全局变量
var (
userMgr *UserMgr
)
type UserMgr struct {
onlineUsers map[int]*UserProcess
}
// 完成对userMgr初始化工作
func init() {
userMgr = &UserMgr{
onlineUsers: make(map[int]*UserProcess, 1024),
}
}
//完成对onlineUsers的增 改
func (this *UserMgr) AddOnlineUser(up *UserProcess) {
this.onlineUsers[up.UserId] = up
}
//完成对onlineUsers的删
func (this *UserMgr) DelOnlineUser(userId int) {
delete(this.onlineUsers, userId)
}
//完成对onlineUsers的查
//返回当前所有在线的用户
func (this *UserMgr) GetAllOnlineUser() map[int]*UserProcess {
return this.onlineUsers
}
//针对点对点聊天,根据Id返回对应的UserProcess
func (this *UserMgr) GetOnlineUserById(userId int) (up *UserProcess,err error) {
//如何从map中取出一个值,带检测的方式
up,ok := this.onlineUsers[userId]
if !ok {//说明你要查找的这个用户当前不在线
err = fmt.Errorf("你要查找的这个用户%d当前不在线",userId)
return
}
return
}
[2] server/process/userProcess.go
[3] common/message/message.go
[4] client/process/userProcess.go
package process
import (
"encoding/json"
"fmt"
"go_code/go_code/chatroom/client/utils"
"go_code/go_code/chatroom/common/message"
"net"
"os"
)
type UserProcess struct{
//暂时不需要字段
}
//关联一个用户登录的方法
//写一个函数,完成一个登录校验
func (this *UserProcess) Login(UserId int, UserPwd string) (err error){
//下一步就要开始定协议(大事)
//fmt.Printf(" userId =%d userPw = %s\n", UserId,UserPwd)
//return nil
//1、链接服务器
conn,err := net.Dial("tcp","localhost:8889")
if err != nil {
fmt.Println("net.Dial err=", err)
return
}
//延时关闭
defer conn.Close()
//2、准备通过conn发送消息给服务器
var mes message.Message
mes.Type = message.LoginMesType
//3、创建一个LoginMesTypMes结构体
var loginMes message.LoginMes
loginMes.UserId = UserId
loginMes.UserPwd = UserPwd
//4、将loginMes序列化
data,err := json.Marshal(loginMes)
if err != nil {
fmt.Println("loginMes json.Marshal err=", err)
return
}
//5、把data赋给来mes.Data字段
mes.Data = string(data)
//6、将mes进行序列化
data,err = json.Marshal(mes)
if err != nil {
fmt.Println(" mes json.Marshal err=", err)
return
}
/*7、到这个时候,data就是我们要发送的消息
//7.1先把data的长度发送给服务器,告诉它要接收多少个字节的内容
//先获取到data的长度-》转成一个表示长度的byte切片
// 错误写法: conn.Write(len(data))
//正确写法:将长度转成一个bytes
//需要用到binarry包里的func (bigEndian) PutUint32(b []byte, v uint32){}
var pkgLen uint32
pkgLen = uint32(len(data))
var buf [4]byte //定义一个数组
binary.BigEndian.PutUint32(buf[0:4],pkgLen)//BigEndian是实例化一个对象,从而可以调用它的方法PutUint32
//发送长度
n,err := conn.Write(buf[:4])
if n!= 4|| err!=nil{
fmt.Println("conn.Write(bytes)fail,err=",err)
return
}
fmt.Printf("客户端,发送数据的长度ok~~长度为%d,内容为:v\n",len(data),string(data))
//发送消息本身
_,err = conn.Write(data)
if err!=nil{
fmt.Println("conn.Write(data)fail,err=",err)
return
}
*/
//以上代码封装进了Writerpkg()
//创建一个Transferi实例
tf := &utils.Transfer{
Conn: conn,
}
//发送data给服务器
err = tf.Writepkg(data)
if err != nil {
fmt.Println("注册发送信息 is error,err=", err)
}
//休眠20
//time.Sleep(20 * time.Second)
//fmt.Println("休眠了20s.......")
//处理服务器返回的消息
//创建一个Transfer实例
tf = &utils.Transfer{
Conn:conn,
}
mes,err = tf.Readpkg() //mes 就是LoginResMes
if err != nil {
fmt.Println("如果readpkg(conn) is error err=", err)
return
}
//8、将mes的Data部分反序列化成loginResMes
var loginResMes message.LoginResMes
err =json.Unmarshal([]byte(mes.Data),&loginResMes)
if err != nil {
fmt.Println("Unmarshal([]byte(mes.Data),&loginResMes) is error,err=", err)
return
}
if loginResMes.Code == 200{
//fmt.Println("登录成功")
//可以显示当前用户列表
fmt.Println("当前在线列表如下:")
for _,v := range loginResMes.UserId{
//如果不显示自己在线
if v == UserId{
continue
}
fmt.Printf("用户id%d在线!\t",v)
}
fmt.Print("\n\n")
//在客户端启动一个协程
//该协程保持和服务器端的一个通讯.如果服务器有数据推送给客户端
//则接收并显示在客户单的终端
go serverProcesee(conn)
//1、循环显示登录成功后的菜单......
ShowMenu()
}else {
fmt.Println(loginResMes.Error)
}
return
}
func (this *UserProcess) Register (UserId int,UserPwd ,UserName string) (err error){
//1、链接服务器
conn,err := net.Dial("tcp","localhost:8889")
if err != nil {
fmt.Println("net.Dial err=", err)
return
}
//延时关闭
defer conn.Close()
//2、准备通过conn发送消息给服务器
var mes message.Message
mes.Type = message.RegisterMesType
//3、创建一个RegisterMes结构体
var registerMes message.RegisterMes
registerMes.User.UserId = UserId
registerMes.User.UserPwd = UserPwd
registerMes.User.UserPwd = UserName
//4、将RegisterMes序列化
data,err := json.Marshal(registerMes)
if err != nil {
fmt.Println("loginMes json.Marshal err=", err)
return
}
//5、把data赋给来mes.Data字段
mes.Data = string(data)
//6、将mes进行序列化
data,err = json.Marshal(mes)
if err != nil {
fmt.Println(" mes json.Marshal err=", err)
return
}
//创建一个Transferi实例
tf := &utils.Transfer{
Conn: conn,
}
//7、发送data给服务器
err = tf.Writepkg(data)
if err != nil {
fmt.Println("注册发送信息 is error,err=", err)
}
//创建一个Transferi实例
tf = &utils.Transfer{
Conn:conn,
}
//8、读取服务器发送的信息
mes,err = tf.Readpkg() //mes 就是RegisterResMes
if err != nil {
fmt.Println("如果Writepkg(conn) is error err=", err)
return
}
//8、将mes的Data部分反序列化成RegisterResMes
var registerResMes message.RegisterResMes
err =json.Unmarshal([]byte(mes.Data),®isterResMes)
if err != nil {
fmt.Println("Unmarshal([]byte(mes.Data),®isterResMes) is error,err=", err)
return
}
if registerResMes.Code == 200{
fmt.Println("注册成功,你重新登录一下")
os.Exit(0)
}else {
fmt.Println(registerResMes.Error)
os.Exit(0)
}
return
}
当一个新的用户上线后,其它已经登录的用户也能获取最新在线用户列表,思路分析、示意图、代码实现
[1] server/process/userProcess.go
package process2
import (
"encoding/json"
"fmt"
"go_code/go_code/chatroom/common/message"
"go_code/go_code/chatroom/server/model"
"go_code/go_code/chatroom/server/utils"
"net"
)
type UserProcess struct {
//字段
Conn net.Conn
//增加一个字段,表示该Conn是哪个用户
UserId int
}
//这里我们编写通知所有在线的用户的方法
//userId 要通知其它的在线用户,我上线
func (this *UserProcess) NotifyOthersOnlineUser(userId int) {
//遍历 onlineUsers, 然后一个一个的发送 NotifyUserStatusMes
for id, up := range userMgr.onlineUsers {
//过滤到自己
if id == userId {
continue
}
//开始通知【单独的写一个方法】
up.NotifyMeOnline(userId)
}
}
func (this *UserProcess) NotifyMeOnline(userId int) {
//组装我们的NotifyUserStatusMes
var mes message.Message
mes.Type = message.NotifyUserStatusMesType
var notifyUserStatusMes message.NotifyUserStatusMes
notifyUserStatusMes.UserId = userId
notifyUserStatusMes.UserStatus = message.UserOnline
//将notifyUserStatusMes序列化
data, err := json.Marshal(notifyUserStatusMes)
if err != nil {
fmt.Println("json.Marshal err=", err)
return
}
//将序列化后的notifyUserStatusMes赋值给 mes.Data
mes.Data = string(data)
//对mes再次序列化,准备发送.
data, err = json.Marshal(mes)
if err != nil {
fmt.Println("json.Marshal err=", err)
return
}
//发送,创建我们Transfer实例,发送
tf := &utils.Transfer{
Conn : this.Conn,
}
err = tf.WritePkg(data)
if err != nil {
fmt.Println("NotifyMeOnline err=", err)
return
}
}
//编写一个函数serverProcessLogin函数, 专门处理登录请求
func (this *UserProcess) ServerProcessLogin(mes *message.Message) (err error) {
//核心代码...
//1. 先从mes 中取出 mes.Data ,并直接反序列化成LoginMes
var loginMes message.LoginMes
err = json.Unmarshal([]byte(mes.Data), &loginMes)
if err != nil {
fmt.Println("json.Unmarshal fail err=", err)
return
}
//1先声明一个 resMes
var resMes message.Message
resMes.Type = message.LoginResMesType
//2在声明一个 LoginResMes,并完成赋值
var loginResMes message.LoginResMes
//我们需要到redis数据库去完成验证.
//1.使用model.MyUserDao 到redis去验证
user, err := model.MyUserDao.Login(loginMes.UserId, loginMes.UserPwd)
if err != nil {
if err == model.ERROR_USER_NOTEXITS{
loginResMes.Code = 500
loginResMes.Error = err.Error()
}else if err == model.ERROR_USER_PWD{
loginResMes.Code = 300
loginResMes.Error = err.Error()
}else {
loginResMes.Code = 505
loginResMes.Error = "服务器内部错误"
}
} else {
loginResMes.Code = 200
//这里,因为用户登录成功,我们就把该登录成功的用放入到userMgr中
//将登录成功的用户的userId 赋给 this
this.UserId = loginMes.UserId
userMgr.AddOnlineUser(this)
通知其它的在线用户, 我上线了
this.NotifyOthersOnlineUser(loginMes.UserId)
//将当前在线用户的id 放入到loginResMes.UsersId
//遍历 userMgr.onlineUsers
for id, _ := range userMgr.onlineUsers {
loginResMes.UserId = append(loginResMes.UserId, id)
}
fmt.Println(user, "登录成功")
}
// //如果用户id= 100, 密码=123456, 认为合法,否则不合法
// if loginMes.UserId == 100 && loginMes.UserPwd == "123456" {
// //合法
// loginResMes.Code = 200
// } else {
// //不合法
// loginResMes.Code = 500 // 500 状态码,表示该用户不存在
// loginResMes.Error = "该用户不存在, 请注册再使用..."
// }
//3将 loginResMes 序列化
data, err := json.Marshal(loginResMes)
if err != nil {
fmt.Println("json.Marshal fail", err)
return
}
//4. 将data 赋值给 resMes
resMes.Data = string(data)
//5. 对resMes 进行序列化,准备发送
data, err = json.Marshal(resMes)
if err != nil {
fmt.Println("json.Marshal fail", err)
return
}
//6. 发送data, 我们将其封装到writePkg函数
//因为使用分层模式(mvc), 我们先创建一个Transfer 实例,然后读取
tf := &utils.Transfer{
Conn : this.Conn,
}
err = tf.WritePkg(data)
return
}
func (this *UserProcess) ServerProcessRegister(mes *message.Message)(err error){
//核心代码...
//1. 先从mes 中取出 mes.Data ,并直接反序列化成RegisterMes
var registerMes message.RegisterMes
err = json.Unmarshal([]byte(mes.Data), ®isterMes)
if err != nil {
fmt.Println("ServerProcessRegister() json.Unmarshal fail err=", err)
return
}
//1先声明一个 resMes
var resMes message.Message
resMes.Type = message.RegisterResMesType
//2在声明一个 RegisterResMes,并完成赋值
var registerResMes message.RegisterResMes
//我们需要到redis数据库去完成注册.
//1.使用model.MyUserDao 到redis去验证
err = model.MyUserDao.Register(®isterMes.User)
if err != nil {
if err == model.ERROR_USER_EXISTS{
registerResMes.Code = 505
registerResMes.Error = err.Error()
} else {
registerResMes.Code = 506
registerResMes.Error = "注册发送未知错误!"
}
} else {
registerResMes.Code = 200
fmt.Println( registerMes.User.UserId,"注册登录成功")
}
//3将 regesterResMes 序列化
data, err := json.Marshal(registerResMes)
if err != nil {
fmt.Println("registerResMes json.Marshal fail", err)
return
}
//4. 将data 赋值给 resMes
resMes.Data = string(data)
//5. 对resMes 进行序列化,准备发送
data, err = json.Marshal(resMes)
if err != nil {
fmt.Println("json.Marshal fail", err)
return
}
//6. 发送data, 我们将其封装到writePkg函数
//因为使用分层模式(mvc), 我们先创建一个Transfer 实例,然后读取
tf := &utils.Transfer{
Conn : this.Conn,
}
err = tf.WritePkg(data)
return
}
[2] sever/proces/userProcess.go [的 Login]
[3] common/mesage/message.go
[4] client/process/userMgr.go
package process
import (
"fmt"
"go_code/go_code/chatroom/common/message"
)
//客户端维护的map
var onlineUsers map[int]*message.User = make(map[int]*message.User,10)
//在客户端显示当前在线的用户
func outputOnlineUser(){
//遍历一把
fmt.Println("当前在线用户列表:")
for id,_ := range onlineUsers{
fmt.Println("用户Id:\t",id)
}
}
//编写一个方法,处理返回的NotifyStatusMes
func UpdateUserStatus (notifyUserStatusMes *message.NotifyUserStatusMes){
//效率比较低,原先可能存在这个user
//user := &message.User{
// UserId : notifyUserStatusMes.UserId,
// UserStatus: notifyUserStatusMes.UserStatus,
//}
//onlineUsers[notifyUserStatusMes.UserId] = user
//适当优化
user , ok := onlineUsers[notifyUserStatusMes.UserId]
if !ok {//原来没有
user = &message.User{
UserId : notifyUserStatusMes.UserId,
//UserStatus: notifyUserStatusMes.UserStatus,
}
}
user.UserStatus = notifyUserStatusMes.UserStatus
onlineUsers[notifyUserStatusMes.UserId] = user
//在客户端显示当前在线的用户
outputOnlineUser()
}
[5] client/process/server.go
package process
import (
"encoding/json"
"fmt"
"go_code/go_code/chatroom/common/message"
"go_code/go_code/chatroom/server/utils"
"net"
"os"
)
//1、显示登录成功后的界面...
func ShowMenu(){
for{
fmt.Println("------恭喜xxx登录成功------")
fmt.Println("------1、显示用户在线列表------")
fmt.Println("------2、发送消息------")
fmt.Println("------3、信息列表------")
fmt.Println("------4、退出系统------")
fmt.Println("请选择(1-4):")
var choose int
fmt.Scanf("%d\n",&choose)
switch choose{
case 1 :
fmt.Println("显示在线用户列表:")
outputOnlineUser()
case 2 :
fmt.Println("发送消息-")
case 3 :
fmt.Println("查看聊天历史记录(存在文件中)-")
case 4 :
fmt.Println("你选择退出了系统-")
os.Exit(0)
default:
fmt.Println("输入错误,请重新输入!")
}
}
}
//2、保持和服务器的通讯
func serverProcesee(conn net.Conn){
//创建一个Transfer实例,不停的读取服务器消息
tf := &utils.Transfer{
Conn: conn,
}
for{
//客户端不停的在读取
fmt.Printf("客户端xxx正在等待读取服务器发送的消息")
mes,err := tf.ReadPkg()
if err != nil {
fmt.Println("tf.Readpkg(),err=", err)
return
}
//如果读取到消息,就是下一步处理逻辑
//fmt.Println("mes=",mes)
switch mes.Type{
case message.NotifyUserStatusMesType:
//通知有人上线了
//1、取出这个NotifyUserStatus
var notifyUserStatusMes message.NotifyUserStatusMes
json.Unmarshal([]byte(mes.Data),¬ifyUserStatusMes)
//2、把这个用户的状态保存到客户端维护的map[int]User中去
UpdateUserStatus(¬ifyUserStatusMes)
default:
fmt.Println("服务器端返回了一个未知的消息类型")
}
}
}
[6] client/process/server.go
18.5.9 实现功能-完成登录用可以群聊
步骤 1:步骤 1:当一个用户上线后,可以将群聊消息发给服务器,服务器可以接收到
思路分析:
代码实现:
[1] common/message/messag.go
[2] client/model/curUser.go
[3] client/process/smsProcess.go 增加了发送群聊消息
[4] 测试
步骤 2:服务器可以将接收到的消息,群发给所有在线用户(发送者除外)
思路分析:
代码实现:
[1] server/process/smsProcess.go
[2] server/main/processor.go
[3] client/process/smsMgr.go
[4] client/process/server.go
18.5.10 聊天的项目的扩展功能要求
- 实现私聊.[点对点聊天]
- 如果一个登录用户离线,就把这个人从在线列表去掉【】
- 实现离线留言,在群聊时,如果某个用户没有在线,当登录后,可以接受离线的消息
- 发送一个文件.