网络编程:
网络协议:
通信双方都遵循,原始协议->标准协议。
从应用的角度出发,协议可以理解为“规则”,是数据传输和数据解释的规则。
假设A,B双方传输文件,规定:
1.传输文件名,接收方接收到文件名,应答返回传输方。
2.发送文件大小,接收方接收该数据返回应答。
3.传输文件内容,接收方接收数据返回应答表示文件内容接收成功。
由此,无论A,B之间传输何种文件,都是通过三次数据传输来完成。A,B之间形成了一个最简单的数据传输规则。双方都按此规则发送、接收数据。A,B之间达成的这个互相遵循的规则即为协议。这种仅在A,B之间被遵守的协议称之为原始协议。
当此协议被更多的人采用,不断地增加、改进、维护、完善。最终形成一个稳定的、完整的文件传输协议,被广泛应用于各种文件传输过程中。该协议就成为一个标准协议。最找的ftp协议就是由此衍生而来。
网络分层架构:
为了减少协议设计的复杂性,大多数网络模型均采用分层的方式来组织。每一层都有自己的功能,就像建筑物一样,每一层都靠下一层支持。每一层利用下一层提供的服务来为上一层提供服务,本层服务的实现细节对上层屏蔽。
越下面的层,越靠近硬件;越上面的层,越靠近用户。
1.物理层:主要定义物理设备标准,如网线的接口类型、光纤的接口类型、各种传输介质的传输速率等。它的主要作用是传输比特流(就是由1、0转化为电流强弱来进行传输,到达目的地后在转化为1、0,也就是我们常说的数模转换与模数转换)。这一层的数据叫做比特。
2.数据链路层:定义了如何让格式化数据以帧为单位进行传输,以及如何让控制对物理介质的访问。这一层通常还提供错误检测和纠正,以确保数据的可靠传输。如:串口通信中使用到的115200、8、N、1 。
3.网络层:在位于不同地理位置的网络中的两个主机系统之间提供连接和路径选择。Internet的发展使得从世界各站点访问信息的用户数大大增加,而网络层正是管理这种连接的层。
4.传输层:定义了一些传输数据的协议和端口号(WWW端口80等),如:TCP(传输控制协议,传输效率低,可靠性强,用于传输可靠性要求高,数据量大的数据),UDP(用户数据报协议,与TCP特性恰恰相反,用于传输可靠性要求不高,数据量小的数据,如QQ聊天数据就是通过这种方式传输的)。 主要是将从下层接收的数据进行分段和传输,到达目的地址后再进行重组。常常把这一层数据叫做段。
5.会话层:通过传输层(端口号:传输端口与接收端口)建立数据传输的通路。主要在你的系统之间发起会话或者接受会话请求(设备之间需要互相认识可以是IP也可以是MAC或者是主机名)。
6.表示层:可确保一个系统的应用层所发送的信息可以被另一个系统的应用层读取。例如,PC程序与另一台计算机进行通信,其中一台计算机使用扩展二一十进制交换码(EBCDIC),而另一台则使用美国信息交换标准码(ASCII)来表示相同的字符。如有必要,表示层会通过使用一种通格式来实现多种数据格式之间的转换。
7.应用层:是最靠近用户的OSI层。这一层为用户的应用程序(例如电子邮件、文件传输和终端仿真)提供网络服务。
层和协议:
每一层都是为了完成一种功能,为了实现这种功能,就需要大家都遵守共同的规则。大家都遵守的这种规则就叫做“协议”(protocol)。
网络的每一层,都定义了很多协议。这些协议的总称,叫“TCP/IP协议”。它是Internet最基本的协议、Internet国际互联网络的基础,由网络层的IP协议和传输层的TCP协议组成。TCP/IP协议是一个大家族,不仅仅只有TCP和IP协议,它还包括其它的协议,如下图:
ARP:正向地址解析协议(Address Resolution Protocol),通过已知的IP,寻找对应主机的MAC地址。
RARP:反向地址转换协议(Reverse Address Resolution Protocol),通过MAC地址确定IP地址。
IP:因特网互联协议(Internet Protocol Address)。
ICMP:Internet控制报文协议(Internet Control Message Protocol),它是TCP/IP协议簇的一个子协议,用于在IP主机、路由器之间传递控制消息。
IGMP:Internet 组管理协议(Internet Group Management Protocol),是因特网协议家族中的一个组播协议。该协议运行在主机和组播路由器之间。
TCP:传输控制协议(Transmission Control Protocol )是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793定义。
UDP:用户数据报协议(User Datagram Protocol),是OSI(Open System Interconnection,开放式系统互联) 参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。
HTTP:超文本传输协议(HyperText Transfer Protocol),是因特网上应用最为广泛的一种网络传输协议,所有的WWW文件都必须遵守这个标准。
FTP:文件传输协议(File Transfer Protocol)是用于在网络上进行文件传输的一套标准协议,使用客户/服务器模式。
每层协议的功能:
1.链路层:以太网规定,连入网络的所有设备,都必须具有”网卡”接口。数据包必须是从一块网卡,传送到另一块网卡。通过网卡能够使不同的计算机之间连接,从而完成数据通信等功能。网卡的地址,就是数据包的发送地址和接收地址,叫做MAC地址。 链路层协议包含的主要休息就是源MAC地址和目的MAC地址。
1+.广播:以太网采用了一种很”原始”的方式,它不是把数据包准确送到接收方,而是向本网络内所有计算机发送广播,让每台计算机自己判断,是否为接收方。
广播发送和接受的步骤:1号计算机向2号计算机发送一个数据包,同一个子网络的3号、4号、5号计算机都会收到这个包。它们读取这个包的”标头”,找到接收方的MAC地址,然后与自身的MAC地址相比较,如果两者相同,就接受这个包,做进一步处理,否则就丢弃这个包。这种发送方式就叫做”广播”(broadcasting)。
2.网络层:作用是引进一套新的地址,使得我们能够区分不同的计算机是否属于同一个子网络。这套地址就叫做”网络地址”,平时我们平时所说的IP地址。这个IP地址好比我们的手机号码,通过手机号码可以得到用户的归属地。
网络地址帮助我们确定计算机所在的子网络,MAC地址则将数据包送到该子网络中的目标网卡。
于是,“网络层”出现以后,每台计算机都有了两种地址,一个是MAC地址,另一个是网络地址。两种地址之间没有任何联系,MAC地址是绑定在网卡上的,网络地址则是管理员分配的,他们只是随机组合在一起。
因此,从逻辑上可以推断,必定是先处理网络地址,然后再处理MAC地址。网络层协议包含的主要信息就是源IP和目的IP。
3.传输层:当我们一边聊QQ,一边聊微信,当一个数据包从互联网上发来的时候,计算机可以通过领完一个参数来确定他是来自QQ的内容还是微信的内容。这种参数表示这个数据包到底提供给哪一个程序(进程)使用。这个参数就叫做“端口”(port),它其实是每个实用网卡的程序的编号。每个数据包都发到主机的特定端口,所以不同的程序就能获得自己所需要的数据。
端口的特点:
对于同一个端口,在不同系统中对应着不同的进程。
对于同一个系统,一个端口只能被一个进程拥有。
4.应用层:应用程序收到“传输层”的数据,接下来就要进行解读。由于互联网是开放架构,数据来源五花八门,必须事先规定好格式,否则根本无法解读。“应用层”的作用就是规定应用程序的数据格式,提取有用的信息。
网络通信的条件总结:
1.网卡,mac地址(不需要用户处理):ARP通过IP找MAC。
2.逻辑地址,IP地址(需要用户指定):确定哪个电脑接收数据。
3.端口,为了确定哪个程序接收数据。
通信过程组包和拆包:由发送者端从应用层向上到链路层组包,再由接收者端从链路层乡下到应用层拆包。
TCP与UDP的区别:
Socket编程:
Socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。Socket就是该模式的一个实现,网络的Socket数据传世以一种特殊的I/O,Socket也是一种文件描述符。Socket也具有一个类似于打开文件的函数调用Socket(),该函数返回一个整型的Socket描述符,随后的链接建立、数据传输等操作都是通过Socker实现的。
常用的Socket类型有两种:流式socket(SOCK_STREAM)和数据报式socket(SOCK_DGRAM)。流式是一种面向连接的Socket,针对于面向连接的TCP服务应用;数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用。
TCP的C/S架构(client/server):
C/S模型:客户端与服务器:需要导入包“net”
客户端对应客户,用来主动请求服务。
服务器对应客服,用来提供服务。
B/S模型(brower/server):浏览器与服务器:
客户端:HTML
TCP服务器代码:
package main
import "fmt"
import "net"
func main() {
//监听
listener, err := net.Listen("tcp", "127.0.0.1:8000")
if err != nil {
fmt.Println("err = ", err)
return
}
defer listener.Close()
//阻塞等待用户链接
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("err = ", err)
continue
}
//接受用户的请求
buf := make([]byte, 1024)
n, err1 := conn.Read(buf)
if err1 != nil {
fmt.Println("err = ", err1)
return
}
fmt.Println("buf = ", string(buf[:n]))
}
defer conn.Close() //关闭当前用户链接
}
netcat工具包的使用:
下载地址:https://eternallybored.org/misc/netcat/
1.解压netcat-win32-1.12
2.拷贝路径,如:C:\netcat-win32-1.12
3.计算机属性->高级系统设置->环境变量(系统变量)->Path配置环境变量添加拷贝的路径
4.命令行启动上面的服务器程序
5.启动cmd链接服务器:nc 127.0.0.1 8000
6.正式输入内容,可以看见服务器端已经捕获了内容并显示,随后关闭链接
TCP客户端代码:
package main
import "fmt"
import "net"
func main() {
//主动连接服务器
conn, err := net.Dial("tcp", "127.0.0.1:8000")
if err != nil {
fmt.Println("err = ", err)
return
}
defer conn.Close()
//发送数据
conn.Write([]byte("hello go!"))
}
简单并发服务器:
package main
import "fmt"
import "net"
import "strings" //字符串处理
//处理请求函数
func HandleConn(conn net.Conn) {
//函数调用完毕自动关闭conn
defer conn.Close()
//获取客户端网络地址信息
addr := conn.RemoteAddr().String()
fmt.Println(addr, " connect successful")
//读取用户数据
buf := make([]byte, 2048)
for{
n, err := conn.Read(buf)
if err != nil {
fmt.Println("err = ", err)
return
}
fmt.Printf("[%s]: = %s\n", addr, string(buf[:n])) //换行也会被传输
//if "exit" == string(buf[:n-2]){ 自编客户端测试时需要排除“\r\n”两个干扰
if "exit" == string(buf[:n-1]){ //排除换行\n干扰
fmt.Println(addr, "exit")
return
}
//把数据做处理:例转换大写
conn.Write([]byte(strings.ToUpper(string(buf[:n]))))
}
}
func main() {
//监听
listener, err := net.Listen("tcp", "127.0.0.1:8000")
if err != nil {
fmt.Println("err = ", err)
return
}
defer listener.Close()
//接收多个用户链接
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("err = ", err)
return
}
//处理用户请求
go HandleConn(conn) //对每一个用户新建协程
}
}
自编客户端:实现即可以输入发送数据也可以接受服务器回复client
package main
import "fmt"
import "net"
import "os"
func main(){
//主动连接服务器
conn, err := net.Dial("tcp", "127.0.0.1:8000")
if err != nil {
fmt.Println("net.Dial err = ", err)
return
}
//main调用完毕,关闭连接
defer conn.Close()
go func(){
//从键盘输入内容给服务器发送
str := make([]byte, 1024)
for{
n, err := os.Stdin.Read(str) //从键盘读取内容
if err != nil {
fmt.Println("os.Stdin.Read err = ", err)
return
}
//向服务器发送内容
conn.Write(str[:n])
}
}()
//接受服务器回复的数据
//切片缓冲
buf := make([]byte, 1024)
for{
n, err := conn.Read(buf) //接收服务器请求
if err != nil {
fmt.Println("conn.Read err = ", err)
return
}
fmt.Println(string(buf[:n])) //字符串形式打印接收到的内容
}
}
文件传输原理:
1.发送者发送文件名给接收者
2.接收者保存文件名并返回状态ok
3.接收者接收状态并开始向接收者发送文件内容(按行发送)
4.接收者读取内容并写入文件(按行接收)
获取文件属性:
package main
import "fmt"
import "os"
func main(){
list := os.Args
if len(list)!=2{
fmt.Println("useage: xxx file")
return
}
fileName := list[1]
info, err := os.Stat(fileName) //返回的info是一个结构体
if err != nil{
fmt.Println("err = ", err)
return
}
fmt.Println("name = ", info.Name)
fmt.Println("size = ", info.Size)
}
传输文件:发送方send
package main
import (
"fmt"
"io"
"net"
"os"
)
//发送文件内容
func SendFile(path string, conn net.Conn) {
//以只读方式打开文件
f, err := os.Open(path)
if err != nil {
fmt.Println("os.Open err = ", err)
return
}
defer f.Close()
buf := make([]byte, 2048)
//读取文件内容,并完整发送
for {
n, err := f.Read(buf) //从文件读取内容
if err != nil {
if err == io.EOF {
fmt.Println("文件发送完毕")
} else {
fmt.Println("f.Read err = ", err)
}
return
}
//发送内容
conn.Write(buf[:n]) //给服务器发送内容
}
}
func main() {
//提示输入文件
fmt.Println("请输入需要传输的文件")
var path string
fmt.Scan(&path)
//获取文件名info.Name()
info, err := os.Stat(path) //返回的info是一个结构体
if err != nil {
fmt.Println("os.Stat err = ", err)
return
}
//主动链接服务器
conn, err1 := net.Dial("tcp", "127.0.0.1:8000")
if err != nil {
fmt.Println("net.Dial err = ", err1)
return
}
defer conn.Close()
//给接收方发送文件名
_, err = conn.Write([]byte(info.Name()))
if err != nil {
fmt.Println("conn.Write err = ", err)
return
}
//接收回复ok,可以发送文件
var n int
buf := make([]byte, 1024)
n, err = conn.Read(buf)
if err != nil {
fmt.Println("conn.Read err = ", err)
return
}
if "ok" == string(buf[:n]) {
//发送文件内容
SendFile(path, conn)
}
}
传输文件:接收方recv
package main
import (
"fmt"
"io"
"net"
"os"
)
//接收文件内容
func RecvFile(fileName string, conn net.Conn) {
//新建文件
f, err := os.Create(fileName)
if err != nil {
fmt.Println("os.Create err = ", err)
return
}
buf := make([]byte, 1024*4)
//完整接收和写入
for {
n, err := conn.Read(buf) //接收文件内容
if err != nil {
if err == io.EOF {
fmt.Println("文件接收完毕")
} else {
fmt.Println("conn.Read err = ", err)
}
return
}
if n == 0 {
fmt.Println("n == 0 文件接收完毕")
break
}
f.Write(buf[:n]) //向文件写入内容
}
}
func main() {
//监听
listener, err := net.Listen("tcp", "127.0.0.1:8000")
if err != nil {
fmt.Println("net.Listen err = ", err)
return
}
defer listener.Close()
//阻塞等待用户连接
conn, err1 := listener.Accept()
if err != nil {
fmt.Println("listener.Accept err = ", err1)
return
}
defer conn.Close()
buf := make([]byte, 1024)
var n int
n, err = conn.Read(buf) //读取文件名
if err != nil {
fmt.Println("conn.Read err = ", err1)
return
}
fileName := string(buf[:n])
//回复状态ok
conn.Write([]byte("ok"))
//接收文件内容
RecvFile(fileName, conn)
}
文件传输流程:启动服务端,启动客户端。在客户端输入需要传输的文件(文件路径\文件名)。所传输的文件将会被创建在服务端程序所在目录。
C/S案例:并发聊天室后台转发服务器
原理分析:
1.保存在线用户:通过map(var onlineMap map[string]Client)添加上线成员(key-value/ip:port-value),key值保存用户的地址和端口(唯一,cliAddr),value保存地址(Addr)、管道(channel)、用户名构成结构体(Client)包含【C chan string(广播使用),Name string、Addr string(与key相同)】。
2.主要协程:处理用户链接
·将用户加入到map
·广播所有在线用户上线信息(message <-上线信息 ):通过定义管道(var message = make(chan string), 用于传输信息),创建新协程for{msg:= <-message}不断检测信息,如果有信息遍历map(for _,cli := range onlineMap{})看有多少在线成员。发送信息时再新创建协程传递参数cli,专门用来发送信息(for msg := range cli.C{ write(msg) }),将信息传给每个成员的C管道(cli.C <- msg)。
·处理用户发送的信息:新建协程,接受用户的请求,把用户发送的内容(buf)转发(message <- buf),如果对方线下就把当前用户从map移除。
第一步代码:广播用户上线
package main
import (
"fmt"
"net"
)
type Client struct {
C chan string //用户发送数据的管道
Name string //用户名
Addr string //网络地址
}
//保存在线用户 cliAddr--->Client
var onlineMap map[string]Client
var message = make(chan string)
//新开协程阻塞转发消息,遍历map发送
func Manager() {
//给map分配空间
onlineMap = make(map[string]Client)
for {
msg := <-message //没有消息则阻塞
//遍历map发送
for _, cli := range onlineMap {
cli.C <- msg
}
}
}
func WriteMsgToClient(cli Client, conn net.Conn) {
for msg := range cli.C { //给当前客户端发送信息
conn.Write([]byte(msg + "\n"))
}
}
func MakeMsg(cli Client, msg string) (buf string) {
buf = "[" + cli.Addr + "]" + cli.Name + ":" + msg
return
}
func HandleConn(conn net.Conn) { //处理用户链接
defer conn.Close()
//获取客户端网络地址
cliAddr := conn.RemoteAddr().String()
//创建一个结构体,默认用户名与网络地址相同
cli := Client{make(chan string), cliAddr, cliAddr}
//结构体添加到map
onlineMap[cliAddr] = cli
//新开协程,专门用于给当前在线用户发送信息
go WriteMsgToClient(cli, conn)
//广播上线信息
message <- MakeMsg(cli, "login")
for { //防止协程停止
}
}
func main() {
//监听
listener, err := net.Listen("tcp", ":8000")
if err != nil {
fmt.Println("net.Listen err = ", err)
return
}
defer listener.Close()
//新开协程阻塞转发消息,遍历map发送
go Manager()
//主协程,循环阻塞等待用户链接
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("listener.Accept err = ", err)
continue
}
go HandleConn(conn) //处理用户链接
}
}
第二步代码:广播消息
package main
import (
"fmt"
"net"
)
type Client struct {
C chan string //用户发送数据的管道
Name string //用户名
Addr string //网络地址
}
//保存在线用户 cliAddr--->Client
var onlineMap map[string]Client
var message = make(chan string)
//新开协程阻塞转发消息,遍历map发送
func Manager() {
//给map分配空间
onlineMap = make(map[string]Client)
for {
msg := <-message //没有消息则阻塞
//遍历map发送
for _, cli := range onlineMap {
cli.C <- msg
}
}
}
func WriteMsgToClient(cli Client, conn net.Conn) {
for msg := range cli.C { //给当前客户端发送信息
conn.Write([]byte(msg + "\n"))
}
}
func MakeMsg(cli Client, msg string) (buf string) {
buf = "[" + cli.Addr + "]" + cli.Name + ":" + msg
return
}
func HandleConn(conn net.Conn) { //处理用户链接
defer conn.Close()
//获取客户端网络地址
cliAddr := conn.RemoteAddr().String()
//创建一个结构体,默认用户名与网络地址相同
cli := Client{make(chan string), cliAddr, cliAddr}
//结构体添加到map
onlineMap[cliAddr] = cli
//新开协程,专门用于给当前在线用户发送信息
go WriteMsgToClient(cli, conn)
//广播上线信息
message <- MakeMsg(cli, "login")
//当前提示
cli.C <- MakeMsg(cli, "I am here")
//新建一个协程,接收用户发送的数据
go func() {
buf := make([]byte, 2048)
for {
n, err := conn.Read(buf)
if n == 0 { //断开或异常
fmt.Println("conn.Read err = ", err)
return
}
msg := string(buf[:n-1]) //通过widows的nc测试会多一个换行
//转发消息
message <- MakeMsg(cli, msg)
}
}()
for {
}
}
func main() {
//监听
listener, err := net.Listen("tcp", ":8000")
if err != nil {
fmt.Println("net.Listen err = ", err)
return
}
defer listener.Close()
//新开协程阻塞转发消息,遍历map发送
go Manager()
//主协程,循环阻塞等待用户链接
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("listener.Accept err = ", err)
continue
}
go HandleConn(conn) //处理用户链接
}
}
第三步代码:who查询在线用户
package main
import (
"fmt"
"net"
)
type Client struct {
C chan string //用户发送数据的管道
Name string //用户名
Addr string //网络地址
}
//保存在线用户 cliAddr--->Client
var onlineMap map[string]Client
var message = make(chan string)
//新开协程阻塞转发消息,遍历map发送
func Manager() {
//给map分配空间
onlineMap = make(map[string]Client)
for {
msg := <-message //没有消息则阻塞
//遍历map发送
for _, cli := range onlineMap {
cli.C <- msg
}
}
}
func WriteMsgToClient(cli Client, conn net.Conn) {
for msg := range cli.C { //给当前客户端发送信息
conn.Write([]byte(msg + "\n"))
}
}
func MakeMsg(cli Client, msg string) (buf string) {
buf = "[" + cli.Addr + "]" + cli.Name + ":" + msg
return
}
func HandleConn(conn net.Conn) { //处理用户链接
defer conn.Close()
//获取客户端网络地址
cliAddr := conn.RemoteAddr().String()
//创建一个结构体,默认用户名与网络地址相同
cli := Client{make(chan string), cliAddr, cliAddr}
//结构体添加到map
onlineMap[cliAddr] = cli
//新开协程,专门用于给当前在线用户发送信息
go WriteMsgToClient(cli, conn)
//广播上线信息
message <- MakeMsg(cli, "login")
//当前提示
cli.C <- MakeMsg(cli, "I am here")
//新建一个协程,接收用户发送的数据
go func() {
buf := make([]byte, 2048)
for {
n, err := conn.Read(buf)
if n == 0 { //断开或异常
fmt.Println("conn.Read err = ", err)
return
}
msg := string(buf[:n-1]) //通过widows的nc测试会多一个换行
if len(msg) == 3 && msg == "who" { //过滤
//遍历map,给当前用户发送所有成员
conn.Write([]byte("user list:\n")) //nc测试中文会出现乱码
for _, tmp := range onlineMap {
msg = tmp.Addr + ":" + tmp.Name + "\n"
conn.Write([]byte(msg))
}
} else {
//转发消息
message <- MakeMsg(cli, msg)
}
}
}()
for {
}
}
func main() {
//监听
listener, err := net.Listen("tcp", ":8000")
if err != nil {
fmt.Println("net.Listen err = ", err)
return
}
defer listener.Close()
//新开协程阻塞转发消息,遍历map发送
go Manager()
//主协程,循环阻塞等待用户链接
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("listener.Accept err = ", err)
continue
}
go HandleConn(conn) //处理用户链接
}
}
第四步代码:修改用户名rename丨name
package main
import (
"fmt"
"net"
"strings"
)
type Client struct {
C chan string //用户发送数据的管道
Name string //用户名
Addr string //网络地址
}
//保存在线用户 cliAddr--->Client
var onlineMap map[string]Client
var message = make(chan string)
//新开协程阻塞转发消息,遍历map发送
func Manager() {
//给map分配空间
onlineMap = make(map[string]Client)
for {
msg := <-message //没有消息则阻塞
//遍历map发送
for _, cli := range onlineMap {
cli.C <- msg
}
}
}
func WriteMsgToClient(cli Client, conn net.Conn) {
for msg := range cli.C { //给当前客户端发送信息
conn.Write([]byte(msg + "\n"))
}
}
func MakeMsg(cli Client, msg string) (buf string) {
buf = "[" + cli.Addr + "]" + cli.Name + ":" + msg
return
}
func HandleConn(conn net.Conn) { //处理用户链接
defer conn.Close()
//获取客户端网络地址
cliAddr := conn.RemoteAddr().String()
//创建一个结构体,默认用户名与网络地址相同
cli := Client{make(chan string), cliAddr, cliAddr}
//结构体添加到map
onlineMap[cliAddr] = cli
//新开协程,专门用于给当前在线用户发送信息
go WriteMsgToClient(cli, conn)
//广播上线信息
message <- MakeMsg(cli, "login")
//当前提示
cli.C <- MakeMsg(cli, "I am here")
//新建一个协程,接收用户发送的数据
go func() {
buf := make([]byte, 2048)
for {
n, err := conn.Read(buf)
if n == 0 { //断开或异常
fmt.Println("conn.Read err = ", err)
return
}
msg := string(buf[:n-1]) //通过widows的nc测试会多一个换行
if len(msg) == 3 && msg == "who" { //过滤
//遍历map,给当前用户发送所有成员
conn.Write([]byte("user list:\n")) //nc测试中文会出现乱码
for _, tmp := range onlineMap {
msg = tmp.Addr + ":" + tmp.Name + "\n"
conn.Write([]byte(msg))
}
} else if len(msg) >= 8 && msg[:6] == "rename"{
//rename mike
name := strings.Split(msg,"丨")[1]
cli.Name = name
onlineMap[cliAddr]=cli
conn.Write([]byte("rename successful\n"))
} else {
//转发消息
message <- MakeMsg(cli, msg)
}
}
}()
for {
}
}
func main() {
//监听
listener, err := net.Listen("tcp", ":8000")
if err != nil {
fmt.Println("net.Listen err = ", err)
return
}
defer listener.Close()
//新开协程阻塞转发消息,遍历map发送
go Manager()
//主协程,循环阻塞等待用户链接
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("listener.Accept err = ", err)
continue
}
go HandleConn(conn) //处理用户链接
}
}
第五步代码:用户退出
package main
import (
"fmt"
"net"
"strings"
)
type Client struct {
C chan string //用户发送数据的管道
Name string //用户名
Addr string //网络地址
}
//保存在线用户 cliAddr--->Client
var onlineMap map[string]Client
var message = make(chan string)
//新开协程阻塞转发消息,遍历map发送
func Manager() {
//给map分配空间
onlineMap = make(map[string]Client)
for {
msg := <-message //没有消息则阻塞
//遍历map发送
for _, cli := range onlineMap {
cli.C <- msg
}
}
}
func WriteMsgToClient(cli Client, conn net.Conn) {
for msg := range cli.C { //给当前客户端发送信息
conn.Write([]byte(msg + "\n"))
}
}
func MakeMsg(cli Client, msg string) (buf string) {
buf = "[" + cli.Addr + "]" + cli.Name + ":" + msg
return
}
func HandleConn(conn net.Conn) { //处理用户链接
defer conn.Close()
//获取客户端网络地址
cliAddr := conn.RemoteAddr().String()
//创建一个结构体,默认用户名与网络地址相同
cli := Client{make(chan string), cliAddr, cliAddr}
//结构体添加到map
onlineMap[cliAddr] = cli
//新开协程,专门用于给当前在线用户发送信息
go WriteMsgToClient(cli, conn)
//广播上线信息
message <- MakeMsg(cli, "login")
//当前提示
cli.C <- MakeMsg(cli, "I am here")
isQuit := make(chan bool) //用户是否主动退出
//新建一个协程,接收用户发送的数据
go func() {
buf := make([]byte, 2048)
for {
n, err := conn.Read(buf)
if n == 0 { //断开或异常
isQuit <- true
fmt.Println("conn.Read err = ", err)
return
}
msg := string(buf[:n-1]) //通过widows的nc测试会多一个换行
if len(msg) == 3 && msg == "who" { //过滤
//遍历map,给当前用户发送所有成员
conn.Write([]byte("user list:\n")) //nc测试中文会出现乱码
for _, tmp := range onlineMap {
msg = tmp.Addr + ":" + tmp.Name + "\n"
conn.Write([]byte(msg))
}
} else if len(msg) >= 8 && msg[:6] == "rename" {
//rename mike
name := strings.Split(msg, "丨")[1]
cli.Name = name
onlineMap[cliAddr] = cli
conn.Write([]byte("rename successful\n"))
} else {
//转发消息
message <- MakeMsg(cli, msg)
}
}
}()
for {
//通过select检查channel流动
select{
case <- isQuit:
delete(onlineMap, cliAddr) //当前用户从map移除
message <- MakeMsg(cli, "logout") //广播用户退出
return
}
}
}
func main() {
//监听
listener, err := net.Listen("tcp", ":8000")
if err != nil {
fmt.Println("net.Listen err = ", err)
return
}
defer listener.Close()
//新开协程阻塞转发消息,遍历map发送
go Manager()
//主协程,循环阻塞等待用户链接
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("listener.Accept err = ", err)
continue
}
go HandleConn(conn) //处理用户链接
}
}
第六步代码:超时处理/自动下线
package main
import (
"fmt"
"net"
"strings"
"time"
)
type Client struct {
C chan string //用户发送数据的管道
Name string //用户名
Addr string //网络地址
}
//保存在线用户 cliAddr--->Client
var onlineMap map[string]Client
var message = make(chan string)
//新开协程阻塞转发消息,遍历map发送
func Manager() {
//给map分配空间
onlineMap = make(map[string]Client)
for {
msg := <-message //没有消息则阻塞
//遍历map发送
for _, cli := range onlineMap {
cli.C <- msg
}
}
}
func WriteMsgToClient(cli Client, conn net.Conn) {
for msg := range cli.C { //给当前客户端发送信息
conn.Write([]byte(msg + "\n"))
}
}
func MakeMsg(cli Client, msg string) (buf string) {
buf = "[" + cli.Addr + "]" + cli.Name + ":" + msg
return
}
func HandleConn(conn net.Conn) { //处理用户链接
defer conn.Close()
//获取客户端网络地址
cliAddr := conn.RemoteAddr().String()
//创建一个结构体,默认用户名与网络地址相同
cli := Client{make(chan string), cliAddr, cliAddr}
//结构体添加到map
onlineMap[cliAddr] = cli
//新开协程,专门用于给当前在线用户发送信息
go WriteMsgToClient(cli, conn)
//广播上线信息
message <- MakeMsg(cli, "login")
//当前提示
cli.C <- MakeMsg(cli, "I am here")
isQuit := make(chan bool) //用户是否主动退出
hasData := make(chan bool) //对方是否有数据发送
//新建一个协程,接收用户发送的数据
go func() {
buf := make([]byte, 2048)
for {
n, err := conn.Read(buf)
if n == 0 { //断开或异常
isQuit <- true
fmt.Println("conn.Read err = ", err)
return
}
msg := string(buf[:n-1]) //通过widows的nc测试会多一个换行
if len(msg) == 3 && msg == "who" { //过滤
//遍历map,给当前用户发送所有成员
conn.Write([]byte("user list:\n")) //nc测试中文会出现乱码
for _, tmp := range onlineMap {
msg = tmp.Addr + ":" + tmp.Name + "\n"
conn.Write([]byte(msg))
}
} else if len(msg) >= 8 && msg[:6] == "rename" {
//rename mike
name := strings.Split(msg, "丨")[1]
cli.Name = name
onlineMap[cliAddr] = cli
conn.Write([]byte("rename successful\n"))
} else {
//转发消息
message <- MakeMsg(cli, msg)
}
hasData <- true //用户键入了数据
}
}()
for {
//通过select检查channel流动
select {
case <-isQuit:
delete(onlineMap, cliAddr) //当前用户从map移除
message <- MakeMsg(cli, "logout") //广播用户退出
return
case <-hasData:
case <-time.After(60 * time.Second): //60s无数据键入自动退出
delete(onlineMap, cliAddr) //当前用户从map移除
message <- MakeMsg(cli, "over time limit") //广播用户退出
return
}
}
}
func main() {
//监听
listener, err := net.Listen("tcp", ":8000")
if err != nil {
fmt.Println("net.Listen err = ", err)
return
}
defer listener.Close()
//新开协程阻塞转发消息,遍历map发送
go Manager()
//主协程,循环阻塞等待用户链接
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("listener.Accept err = ", err)
continue
}
go HandleConn(conn) //处理用户链接
}
}
以上六步完成了建议的并发聊天室,每一步都在上一步的基础上完成了一些功能和优化。