Contents
Go的网络编程
通信协议
通信协议也叫网络传输协议或简称为传送协议(Communications Protocol),是指计算机通信或网络设备的共同语言。
现在最普及的计算机通信为网络通信,所以“传送协议”一般都指计算机通信的传送协议,如:TCP/IP、NetBEUI、HTTP、FTP等。
然而,传送协议也存在于计算机的其他形式通信,例如:面向对象编程里面对象之间的通信;操作系统内不同程序之间的消息,都需要有一个传送协议,以确保传信双方能够沟通无间。
协议 | 解释 |
---|---|
传输层 | 常见协议有TCP/UDP协议。 |
应用层 | 常见的协议有HTTP协议,FTP协议。 |
网络层 | 常见协议有IP协议、ICMP协议、IGMP协议。 |
网络接口层 | 常见协议有ARP协议、RARP协议。 |
TCP传输控制协议 | (Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。 |
UDP用户数据报协议 | (User Datagram Protocol)是OSI参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。 |
HTTP | 超文本传输协议(Hyper Text Transfer Protocol)是互联网上应用最为广泛的一种网络协议。 |
FTP | 文件传输协议(File Transfer Protocol) |
IP协议 | 是因特网互联协议(InternetProtocol) |
ICMP协议 | 是Internet控制报文协议(Internet Control Message Protocol)它是TCP/IP协议族的一个子协议,用于在IP主机、路由器之间传递控制消息。 |
IGMP协议 | 是Internet组管理协议(Internet Group Management Protocol),是因特网协议家族中的一个组播协议。该协议运行在主机和组播路由器之间。 |
ARP协议 | 是正向地址解析协议(Address Resolution Protocol),通过已知的IP,寻找对应主机的MAC地址。 |
RARP | 是反向地址转换协议,通过MAC地址确定IP地址。 |
Socket编程
对于底层网络应用开发者而言,几乎所有网络编程都是Socket,因为大部分底层网络的编程都离不开Socket编程。HTTP编程、Web开发、IM通信、视频流传输的底层都是Socket 编程。
- Socket又称”套接字”,应用程序通常通过”套接字”向网络发出请求或者应答网络请求,使主机间或者一台计算机上的进程间可以通讯。
-
可以把Socket理解成类似插座的东西, 通过Socket就可以发送和接受数据了, 就像插座插上电器之后就可以向外提供电能了.
-
TCP编程的客户端和服务器端都是通过Socket来完成的.其实UDP协议通信也是使用的套接字, 和TCP协议稍有差别. TCP是面向连接的套接字, 而UDP是面向无连接的套接字.
简单的套接字原理图
在TCP/IP协议中,
IP地址+TCP或UDP端口号
唯一标识网络通讯中的一个进程。“IP地址+端口号”就对应一个socket。欲建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。因此可以用Socket来描述网络连接的一对一关系。
常用的Socket类型有两种:流式Socket(SOCK_STREAM)和数据报式Socket(SOCK_DGRAM)。流式是一种面向连接的Socket,针对于面向连接的TCP服务应用;数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用。
常见的网络设计模式
C/S模式
- 传统的网络应用设计模式,客户机(client)/服务器(server)模式。需要在通讯两端各自部署客户机和服务器来完成数据通信。
B/S模式
- 浏览器(Browser)/服务器(Server)模式。只需在一端部署服务器,而另外一端使用每台PC都默认配置的浏览器即可完成数据的传输。
优缺点
- 对于C/S模式来说,其优点明显。客户端位于目标主机上可以保证性能,将数据缓存至客户端本地,从而提高数据传输效率。且,一般来说客户端和服务器程序由一个开发团队创作,所以他们之间所采用的协议相对灵活。可以在标准协议的基础上根据需求裁剪及定制。
-
B/S模式相比C/S模式而言,由于它没有独立的客户端,使用标准浏览器作为客户端,其工作开发量较小。只需开发服务器端即可。另外由于其采用浏览器显示数据,因此移植性非常好,不受平台限制。如早期的在线网页游戏,在各个平台上都可以完美运行。
-
B/S模式的缺点也较明显。由于使用第三方浏览器,因此网络应用支持受限。另外,没有客户端放到对方主机上,缓存数据不尽如人意,从而传输数据量受到限制。应用的观感大打折扣。第三,必须与浏览器一样,采用标准http协议进行通信,协议选择不灵活。
TCP的架构
net包提供的函数
func(c*TCPConn)Write(b []byte)(n int,err os.Error)
func(c*TCPConn)Read(b []byte)(n int,err os.Error)
-> TCPConn 可以用在客户端和服务器端读写数据。
-> 还需要知道一个TCPAddr类型,它表示一个TCP的地址信息
type TCPAddr struct{
IPIP
Port int
Zone string//IPv6范围寻址区域
}
- 在Go 语言中通过ResolveTCPAddr获取一个TCPAddr:
- func ResolveTCPAddr(net,addr string)(*TCPAddr,os.Error)
参数解释:
net参数是TCP4/TCP6/TCP中的任意一个,分别表示TCP(IPv4-only),TCP(IPv6-only)或者TCP(IPv4,IPv6的任意一个)
addr表示域名或者IP地址
Server端函数
Listen函数:
func Listen(network, address string) (Listener, error)
network:选用的协议:TCP、UDP, 如:“tcp”或 “udp”
address:IP地址+端口号, 如:“127.0.0.1:8000”或 “:8000”
Listener 接口:
type Listener interface {
Accept() (Conn, error)
Close() error
Addr() Addr
}
Conn 接口:
type Conn interface {
Read(b []byte) (n int, err error)
Write(b []byte) (n int, err error)
Close() error
LocalAddr() Addr
RemoteAddr() Addr
SetDeadline(t time.Time) error
SetReadDeadline(t time.Time) error
SetWriteDeadline(t time.Time) error
}
Client端
Go语言通过net包中的DialTCP函数来建立一个TCP连接,并返回一个TCPConn类型的对象,当连接建立时服务器端也创建一个同类型的对象,此时客户端和服务器端通过各自拥有的TCPConn对象进行数据交换。一般而言,客户端通过TCPConn对象将请求信息发送到服务器端,读取服务器端响应的信息。服务器端读取并解析来自客户端的请求,并返回应答信息,这个连接只有当任一端关闭连接之后才失效,不然该连接可以一直使用。
func DialTCP(net string,laddr,raddr*TCPAddr)(c*TCPConn,err os.Error)
- net参数是TCP4、TCP6、TCP中的任意一个,分别表示TCP(IPv4-only)、TCP(IPv6-only)或者TCP(IPv4、IPv6的任意一个)
-
laddr表示本机地址,一般设置为nil;
-
raddr表示远程服务地址。
Dial函数:
func Dial(network, address string) (Conn, error)
network:选用的协议:TCP、UDP,如:“tcp”或 “udp”
address:服务器IP地址+端口号, 如:“121.36.108.11:8000”或 “www.itcast.cn:8000”
Conn 接口:
type Conn interface {
Read(b []byte) (n int, err error)
Write(b []byte) (n int, err error)
Close() error
LocalAddr() Addr
RemoteAddr() Addr
SetDeadline(t time.Time) error
SetReadDeadline(t time.Time) error
SetWriteDeadline(t time.Time) error
}
简单的C/S架构通讯
- 服务端
func main() {
//创建监听
listener,err := net.Listen("tcp",":8000")
if err != nil {
fmt.Println("listen err:",err)
return
}
defer listener.Close() //主main结束自动关闭监听
fmt.Println("服务端等待建立连接。。。")
//监听,等待客户端的请求连接
conn,err := listener.Accept()
if err != nil {
fmt.Println("accept err",err)
}
buf := make([]byte,1024) //建立读取的缓冲区
n,err := conn.Read(buf)
if err != nil {
fmt.Println("Read err",err)
return
}
fmt.Println("服务端读取到:",string(buf[:n]))
}
- 客户端
func main() {
//主动发起连接请求
conn, err := net.Dial("tcp", "docker.poph163.com:8000")
if err != nil {
fmt.Println("Dial err",err)
return
}
defer conn.Close()
//发送数据
_,err = conn.Write([]byte("Are You Ok?"))
if err != nil {
fmt.Println("Write err:",err)
return
}
}
TCP状态转换
TCP状态转换图
- 主动发起连接请求端:CLOSED() -> 完成三次握手 -> ESTABLISEHED(数据通信状态) -> Dial0函数返回
-
被动发起连接请求端:CLOSED -> 调用Accept0函数 -> LISTEN -> 完成三次握手 -> ESTABLISEHED(数据通信状态) -> Accept0函数返回 -> 数据传递期间 -> ESTABLISEHED(数据通信状态)
-
主动关闭连接请求端
- ESTABLISEHED -> FIN_WAIT_2(半关闭) -> TIME_WAIT -> 2MSL -> 确认最后一个ACK被对端成功接收。 -> CLOSE半关闭、TIME_WAIT、2MSL -> 只会出现在“主动关闭连接请求端”
-
被动关闭连接请求端:ESTABLISEHED——CLOSE
并发的C/S通讯
Accept()函数的作用是等待客户端的链接,如果客户端没有链接,该方法会阻塞。如果有客户端链接,那么该方法返回一个Socket负责与客户端进行通信。所以,每来一个客户端,该方法就应该返回一个Socket与其通信,因此,可以使用一个死循环,将Accept()调用过程包裹起来。
服务端
- 1.创建监听套接字listener:=net.Listen(”tcp”,服务器的IP+port) //tcp不能大写
-
2.defer listener.Close()
-
3.for 循环阻塞监听客户端连接事件conn:=listener.Accept()
-
4.创建go程对应每一个客户端进行数据通信go HandlerConnet05.实现HandlerConnet(conn net.Conn)
- (1)defer conn.Close()
-
(2)获取成功连接的客户端Addrconn.RemoteAddr()
-
(3)for 循环读取客户端发送数据conn.Read(buf)
-
(4)处理数据小——大strings.ToUpper05)回写转化后的数据conn.write(buf:n)
func main() {
//创建监听套接字
listener,err := net.Listen("tcp",":8000")
checkerr(err,"net.Listen")
defer listener.Close()
//监听客户端连接请求
for {
conn,err := listener.Accept()
//checkerr(err)
checkerr(err,"listener.Accept")
go HandlerConnect(conn)
//defer conn.Close()
}
//具体完成服务器和客户端的数据通信
//go HandlerConnect(conn)
}
func checkerr(err error,name string) {
if err != nil {
if name == "conn.Read" {
new := fmt.Sprintf("%s",err)
if new == "EOF" {
fmt.Println("!!!服务器接受到客户端请求关闭!!!")
return
}
if err != nil{
fmt.Println(name,"errer:",err)
return
}
}else {
_, _ = fmt.Fprintf(os.Stderr,name ," %s", err.Error())
}
return
}
}
func HandlerConnect(conn net.Conn) {
defer conn.Close()
//获取连接的客户端的网络地址
addr := conn.RemoteAddr()
fmt.Println("-> ",addr,"客户端成连接")
//循环读取直到断开连接
buf := make([]byte,1024)
for {
n,err := conn.Read(buf)
//checkerr(err)
checkerr(err,"conn.Read")
if n == 0 {
fmt.Println("【客户端已经断开】 ->",addr)
return
}
fmt.Println("服务端读取到:",string(buf[:n]))
conn.Write([]byte(strings.ToUpper(string(buf[:n]))))
}
//fmt.Println("服务端读取到:",string(buf[:n]))
//小写转化大小回复客户端
}
客户端
//Tcp客户端
func main() {
//与服务器建立连接
conn,err := net.Dial("tcp","docker.poph163.com:8000")
if err != nil {
fmt.Println("net.Dial Error",err)
return
}
defer conn.Close()
go func() {
str := make([]byte,4088)
for {
//获取用户的标准输入
n,err := os.Stdin.Read(str)
if err != nil {
fmt.Println("os.stdin.Read Error:",err)
continue
}
//写给服务端
conn.Write(str[:n])
}
}()
//读服务器回复,并以大写回复服务器
buf := make([]byte,4000)
for {
n,err := conn.Read(buf)
if n == 0 {
fmt.Println("与服务器连接断开!")
return
}
if err != nil {
fmt.Println("conn.Read err",err)
return
}
fmt.Println("客户端读取成功回复服务器",string(buf[:n]))
conn.Write([]byte(strings.ToLower(string(buf[:n])))) //双方互怼停不下来
}
}
UDP通讯
由于UDP是“无连接”的,所以,服务器端不需要额外创建监听套接字,只需要指定好IP和port,然后监听该地址,等待客户端与之建立连接,即可通信。
- UDP也叫用户数据报协议。UDP是面向无连接的, 一方负责发送数据(客户端), 只要知道对方(接受数据:服务器) 的地址就可以直接发数据了, 但是能不能达到就没有办法保证了
-
UDP编程相比TCP编程简单了很多,因为UDP不是面向连接的, 而是面向无连接的。TCP是面向连接的, 客户端和服务端必须连接之后才能通讯, 就像打电话, 必须先接通才能通话.
-
虽然用UDP传输面向无连接, 数据不可靠,但它的优点是和TCP比,速度快,对于不要求可靠到达的数据,就可以使用UDP协议。 比如局域网的视频同步, 使用 udp 是比较合适的:快, 延迟越小越好
创建监听地址:
func ResolveUDPAddr(network, address string) (*UDPAddr, error)
创建监听连接:
func ListenUDP(network string, laddr *UDPAddr) (*UDPConn, error)
接收udp数据:
func (c *UDPConn) ReadFromUDP(b []byte) (int, *UDPAddr, error)
写出数据到udp:
func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (int, error)
服务端
- 1.创建 server端地址结构(IP+port)net.ResolveUDPAddr()
-
2.创建用于通信的socket,绑定地址结构 udpConn=net.ListenUDPO
-
3.defer udpConn.Close()
-
4.读取客户端发送数据ReadFromUDP()返回:n,cltAddr(客户端的IP+port),err
-
5.写数据给客户端WriteToUDP(”待写数据”,cltAddr)
func main() {
//组织一个UDP地址结构,udpaddr是地址结构
udpaddr,err :=net.ResolveUDPAddr("udp","127.0.0.1:9000")
checkerr(err,"net.ResolveUDPAddr")
fmt.Println("构建UDP服务器地址结构结束")
//创建用户通讯的socket
udpconn,err := net.ListenUDP("udp",udpaddr)
checkerr(err,"net.ListenUDP")
fmt.Println("监听端口完成!等待数据通讯...")
defer udpconn.Close()
//读取客户端发送的数据
buf := make([]byte,3000)
//返回三个值:读取到的字节数,客户端地址,err
n,ipaddr,err := udpconn.ReadFromUDP(buf)
checkerr(err,"udpconn.ReadFromUDP")
//模拟数据处理
fmt.Printf("服务器读取到 %v 数据,%s\n",ipaddr,string(buf[:n]))
//提取当前的系统时间
nowday := time.Now().String()
//写数据到服务端
_,err = udpconn.WriteToUDP([]byte(nowday),ipaddr)
checkerr(err,"udpconn.WriteToUDP")
}
func checkerr(err error,name string) {
if err != nil {
fmt.Printf("%s Error :%s",name,err)
return
}
}
客户端
使用TCP即可将net.Dial("udp","ip:port") //即可
UDP和TCP的差异
TCP | UDP |
---|---|
面向连接 | 面向无连接 |
要求系统资源较多 | 要求系统资源较少 |
TCP程序结构较复杂 | UDP程序结构较简单 |
使用流式 | 使用数据包式 |
保证数据准确性 | 不保证数据准确性 |
保证数据顺序 | 不保证数据顺序 |
通讯速度较慢 | 通讯速度较快 |
- 优点 => TCP:稳定、安全、有序。 –> UDP:效率高、开销小。开发复杂度低。
-
缺点 => TCP效率低、开销大。开发复杂度高。 –> UDP:稳定性查、安全低、无序。
使用场景
- TCP:对数据传输安全性、稳定性要求较高的场合。网络文件传输。下载、上传。
-
UDP:对数据实时传输要求较高的场合。视频直播、在线电话会议。游戏
文件传输
原理图
- 首先获取文件名。借助os包中的stat()函数来获取文件属性信息。在函数返回的文件属性中包含文件名和文件大小。Stat参数name传入的是文件访问的绝对路径。FileInfo中的Name()函数可以将文件名单独提取出来。
func Stat(name string) (FileInfo, error)
type FileInfo interface {
Name() string
Size() int64
Mode() FileMode
ModTime() time.Time
IsDir() bool
Sys() interface{}
}
- 获取文件属性
func main() {
list := os.Args //获取命令行参数
if len(list) !=2 { //确保用户输入一个命令参数
fmt.Println("格式:xxx.go 文件名")
return
}
filename := list[1] //从命令行保存文件名包括路径
fileinfo ,err := os.Stat(filename) //根据文件名获取出文件的属性信息
if err != nil {
fmt.Printf("os.stat Error:",err)
return
}
fmt.Printf("当前的文件名:%s\n当前的文件大小为:%dbyte",fileinfo.Name(),fileinfo.Size())
}
调试结果:
C:\golearn>go run demo.go qq.txt
当前的文件名:qq.txt
当前的文件大小为:21byte
发送端的实现
规划大致编写的方针如下所示:
- 1.提示用户输入文件名。接收文件名path(含访问路径)
-
2.使用os.Stat()获取文件属性,得到纯文件名(去除访问路径)
-
3.主动连接服务器,结束时关闭连接
-
4.给接收端(服务器)发送文件名conn.Write()
-
5.读取接收端回发的确认数据conn.Read()
-
6.判断是否为“ok”。如果是,封装函数SendFile() 发送文件内容。传参path和conn
-
7.只读Open文件, 结束时Close文件
-
8.循环读文件,读到EOF终止文件读取
-
9.将读到的内容原封不动Write给接收端(服务器)
func main() {
list := os.Args //获取命令行参数
if len(list) !=2 { //确保用户输入一个命令参数
fmt.Println("格式:xxx.go 文件名")
return
}
filename := list[1] //从命令行保存文件名包括路径
fileinfo ,err := os.Stat(filename) //根据文件名获取出文件的属性信息
if err != nil {
fmt.Printf("os.stat Error:",err)
return
}
//客户端主动发起连接
conn,err :=net.Dial("tcp","docker.poph163.com:8000")
checkerr(err,"net.Dial")
defer conn.Close()
//发送文件名字给接收端
_,err =conn.Write([]byte(string(filename)))
checkerr(err,"conn.Write")
fmt.Printf("当前已经将%s文件名发送给目标机器!",filename)
//读取服务器是否接受成功
buf := make([]byte,2048)
n,err := conn.Read(buf)
checkerr(err,"conn.Read")
//写文件内容给服务器
if "ok" == string(buf[:n]) {
fmt.Println("对端已经创建文件名,开始传输!")
sendfile(conn,filename,fileinfo.Size())
}
}
func checkerr(err error,name string) {
if err != nil {
fmt.Printf("%s Error -> %s",name,err)
return
}
}
//发送文件
func sendfile(conn net.Conn,filePath string,size int64) {
//只读打开文件
f , err :=os.Open(filePath)
checkerr(err,"os.open")
defer f.Close()
//从本地文件中读取数据写给接收端
buf := make([]byte,size)
for {
n,err := f.Read(buf)
if n == 0 {
fmt.Println("传输结束!")
return
}
//写到socket中
_,err = conn.Write(buf[:n])
checkerr(err,"conn.write")
}
}
接收端实现
- 1.创建监听listener,程序结束时关闭。
-
2.阻塞等待客户端连接,程序结束时关闭conn。
-
3.读取客户端发送文件名。保存fileName。
-
4.回发“ok”给客户端做应答
-
5.封装函数 RecvFile接收客户端发送的文件内容。传参fileName 和conn
-
6.按文件名Create文件,结束时Close
-
7.循环Read客户端发送的文件内容,当读到EOF说明文件读取完毕。
-
8.将读到的内容原封不动Write到创建的文件中
func main() {
//监听socke
listener ,err := net.Listen("tcp",":8000")
checkerr(err,"net.Listen")
defer listener.Close()
//阻塞监听
conn,err := listener.Accept()
checkerr(err,"listener.Accept")
defer conn.Close()
//获取文件名并且保存
buf := make([]byte,1024)
n,err := conn.Read(buf)
checkerr(err,"conn.Read")
recfile(conn,string(buf[:n]))
//filename:= "asdasdasd"
//_,err =conn.Write([]byte(string(filename)))
//checkerr(err,"conn.Write")
}
func checkerr(err error,name string) {
if err != nil {
fmt.Printf("%s Error:%s\n",name,err)
return
}
}
func recfile(conn net.Conn,file string) {
f,err:=os.Create(file)
checkerr(err,"os.Creat")
fmt.Println("创建文件名成功")
defer f.Close()
//执行完成创建
_,err = conn.Write([]byte("ok"))
checkerr(err,"conn.Write")
//网络传输读取文件并且保存在本地
buf := make([]byte,4089)
fmt.Printf("正在接收文件。。。")
//一直读文件直到读取全部结束
for {
n,err := conn.Read(buf)
fmt.Printf("...")
checkerr(err,"client conn.Read")
if n == 0 {
fmt.Println("接收文件结束")
return
}
f.Write(buf[:n])
}
}
效果图
聊天室
设计架构图
设计理念
- 1.主协程(服务器):
- 负责监听、接收用户(客户端)连接请求,建立通信关系。同时启动相应的协程处理任务。
- 2.处理用户连接协程:HandleConnect
- 负责新上线用户的存储,用户消息读取、发送,用户改名、下线处理及超时处理。为了提高并发效率,同时给一个用户维护多个协程来并行处理上述任务。
- 3.用户消息广播协程:Manager
- 负责在线用户遍历,用户消息广播发送。需要与HandleConnect协程及用户子协程协作完成。
- 4.协程间应用数据及通信:
- map:存储所有登录聊天室的用户信息, key:用户的ip+port。Value:Client结构体。
- Client结构体:包含成员:用户名Name,网络地址Addr(ip+port),发送消息的通道C(channel)
- 通道message:协调并发协程间消息的传递。
定义全局map 存储在线用户信息。Key为用户网络地址。Value为用户结构体。
主协程,监听客户端连接请求,当有新的客户端连接,创建新协程handleConnet处理用户连接。
handleConnet协程,获取用户网络地址(Ip+port),创建新用户结构体,包含成员C、Name、Addr。新用户的Name和Addr初值都是用户网络地址(Ip+port)。将用户结构体存入map中。并创建WriteMsgToClient协程,专门负责给当前用户发送消息。组织新用户上线广播消息内容,写入全局通道message中。
WriteMsgToClient协程,读取用户结构体C中的数据,没有则阻塞等待,有数据写出给登录用户。
Manager协程,给map分配空间。循环读取 message 通道中是否有数据。没有,阻塞等待。有则解除阻塞,将message通道中读到的数据写到用户结构体中的C通道。
服务端
-
1.主go程中,创建监听Socket套接字。
-
2.for 循环监听户端连接请求。Accept()
-
3.有一个户端连接,创建新go程处理户端数据 HandlerConnet(conn)defer
-
4.定义全局结构体类型C、Name、Addr
-
5.创建全局map、channel
-
6.实现HandlerConnet,获取户端IP+port—>RemoteAddr()。初始化新用户结构体信息。name=Addr
-
7.创建Manager 实现管理go程。Accept()之前。
-
8.实现Manager。初始化在线用户map。循环读取全局 channel,如果无据,阻。如果有数据,遍历在线用户map,将数据写到用户的C里
-
9.将新用户添加到在线用户map中。Key==IP+port value=新用户结构体
-
10.创建WriteMsgToClient go程,专门给当前用户写数据。——来源于用户自带的C中
-
11.实现WriteMsgToClient(clnt,conn)。遍历自带的C,读数据,conn.Write到奋户端。
-
12.HandlerConnet中,结束位置,组织用户上线信息,将用户上线信息写到全局 channel——Manager的读就被激活(原来一直被阻塞)
-
13.HandlerConnet中结尾使用,for{;}保证进程不结束
package main
import (
"fmt"
"net"
"strings"
"time"
)
//定义每个客户端的维护通道和信息
type client struct {
name string
addr string
C chan string
}
//全局性Message监听是否是消息通信的
var message = make(chan string)
//全局Map字典存储client信息,Key:IP+Port,Value:client信息
var onlinClient map[string]client
//处理所有的Client接入信息
func handleconnect(conn net.Conn) {
fmt.Println("开启成功")
conn.Write([]byte("查看在线用户请使用【who】,改名格式:【Rename|xxx】 -> 【|】为分隔符\n"))
defer conn.Close()
//获取用户的连接地址
ipPort :=conn.RemoteAddr().String()
fmt.Printf("当前上线了【%s】用户 => 在线\n",ipPort)
//创建用户连接结构体
user := client{"test",ipPort,make(chan string)}
//将连接的用户,添加到在线用户表Map中(key:ip+port,value:client)
onlinClient[ipPort] = user
//创建专门用来给当前用户发送消息的Gorouting
go WriteMessageClient(user,conn)
//发送用户上线消息到全局Message中去
message <- MakeMessage(user,"login")
//创建用于用户下线的channel
Quit := make(chan bool)
//创建用于检测用户超时的channel
hashDate := make(chan bool)
//创建匿名Goroutine,专门处理用户发送的消息
go func() {
buf := make([]byte,2048)
for {
n ,err := conn.Read(buf)
if n == 0 {
fmt.Printf("检测到客户端:【%s】 => 退出\n",user.name)
return
}
checkerr(err,"匿名conn.Read")
//将读取到的用户消息,写入到message中
msg := string(buf[:n-1])
//获取用户在线列表
if msg == "who" && len(msg) == 3{
conn.Write([]byte("当前在线用户:\n"))
//循环遍历OnlineCline字典中的在线用户信息
for _,value := range onlinClient {
UserInfo := value.addr + ":" + "上线" + value.name + "\n"
conn.Write([]byte(UserInfo))
}
}else if len(msg) >=8 && msg[:6] == "Rename" {
Rename := strings.Split(msg,"|")[1]
user.name = Rename
onlinClient[ipPort] = user
conn.Write([]byte("Your Rename test ->"+ Rename + "is Successful"+ "\n"))
}else {
//将参数写到channel中去触发WriteClient
message <- MakeMessage(user,msg)
}
hashDate <- true
}
}()
//检测用户主动下线
go func() {
buf := make([]byte,2048)
for {
n,err := conn.Read(buf)
if n == 0 {
Quit <- true
//fmt.Printf("用户【%s】已经下线",user.name)
return
}
checkerr(err,user.name+"下线异常 ->")
}
}()
//处理掉下线的Map-key
for {
//监听数据的流动
select {
case <-Quit:
delete(onlinClient, ipPort) //将用户从Map中移除
message <- MakeMessage(user, "当前用户已经下线") //写入用户下线全局发送
return
case <-hashDate:
case <-time.After(60 * time.Second):
delete(onlinClient,ipPort)
message <- MakeMessage(user,"timeout!!!")
return
}
}
}
//用户消息函数,组合所有的需要的参数
func MakeMessage(user client,msg string) (buf string) {
buf = "【" + user.addr + "】" + user.name + ":" + msg
return buf
}
//创建全局channel处理传递用户消息信息
func WriteMessageClient(user client,conn net.Conn) {
//监听用户自带的channel上是否有消息,有则发送给所有的用户
for message := range user.C {
conn.Write([]byte(message + "\n"))
}
}
//创建Manager
func Manager() {
//初始化onlineMap
onlinClient = make(map[string]client)
//监听全局的channel中是否存在数据,有数据存储没有数据阻塞
for {
msg := <- message
//循环发送消息给所有的的在线用户
for _, user := range onlinClient {
user.C <- msg
}
}
}
func checkerr(err error,name string) {
if err != nil {
fmt.Printf("【%s】 当前发生异常 Error -> %s\n",name,err)
return
}
}
func main() {
//创建监听的套接字
listener,err := net.Listen("tcp",":8000")
checkerr(err,"net.Listen")
defer listener.Close()
//创建管理者,管理Map和全局的channel
go Manager()
//循环的监听客户端的连接请求
for {
conn,err := listener.Accept()
checkerr(err,"Listerner.Accept")
//循环启动Goroutine处理客户端请求
go handleconnect(conn)
}
}
哇,感觉很坚持呢,在找 Go 的工作吗?
面向生活编程,未来可能有打算
Excellent website. Lots of useful information here. I am sending it to a few friends ans also sharing in delicious. And obviously, thanks for your sweat!