Socket编程
什么是Socket
Socket起源于Unix,==而Unix基本哲学之一就是“一切皆文件”== ,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。Socket就是该模式的一个实现,网络的Socket数据传输是一种特殊的I/O,Socket也是一种文件描述符。==Socket也具有一个类似于打开文件的函数调用:Socket(),该函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。==
常用的Socket类型有两种:==流式Socket(SOCK_STREAM)和数据报式Socket(SOCK_DGRAM)。== 流式是一种面向连接的Socket,针对于面向连接的TCP服务应用;数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用。
TCP的C/S架构
CS模型介绍
cs模型(client and server ):客户端和服务器
客户端 =======> 客户
1)主动请求服务
服务器 ========》 客服
2)被动提供服务
brower/server
bs模型
brower =====》 客户端, html
示例程序1
服务器代码
package main
import (
"fmt"
"net"
)
func main() {
//监听
listener, err := net.Listen("tcp", "127.0.0.1:8001") //使用Listen进行监听
//判断是否有错误,有错误就不要执行了
if err != nil {
fmt.Println("err = ", err)
return
}
defer listener.Close() //最后使用 延迟调用函数defer 关闭监听
//阻塞等待用户链接
conn, err := listener.Accept() //调用方法
if err != nil {
fmt.Println("err = ", err)
return
}
//接收用户的请求
buf := make([]byte, 1024) //创建1024大小的缓冲区切片
n, err1 := conn.Read(buf) //将接收到的内容进行读取到缓冲区,n是占用了多少的切片
if err1 != nil {
fmt.Println("err1 = ", err1)
return
}
fmt.Println("buf = ", string(buf[:n])) //将切片转换为string进行打印
defer conn.Close() //关闭当前用户链接,前面还有关闭监听,先关闭当前用户链接再关闭监听,defer的先进后出
}
Netcat 安装使用教程
- 将压缩包解压到常用的软件安装盘
- 有两个可执行文件
- 添加环境变量
- 使用
客服端代码
package main
import (
"fmt"
"net"
)
func main() {
//主动连接服务器,使用的是Dial函数
conn, err := net.Dial("tcp", "127.0.0.1:8001")
if err != nil {
fmt.Println("err = ", err)
return
}
defer conn.Close() //最后关闭连接
//发送数据
conn.Write([]byte("are u ok?")) //发送字节切片(类型转换)
}
运行结果
简单版并发服务器
TCP服务器
package main
import (
"fmt"
"net"
"strings"
)
//处理用户请求
func HandleConn(conn net.Conn) { //函数传递的类型是net.Conn
//函数调用完毕,自动关闭conn
defer conn.Close()
//获取客户端的网络地址信息 RemoteAddr 远程地址
addr := conn.RemoteAddr().String() //获取地址信息转换成字符串
fmt.Println(addr, " conncet sucessful")
buf := make([]byte, 2048) //定义一个切片接收数据
for {
//读取用户数据
n, err := conn.Read(buf) //使用Read函数读取数据到buf中,并返回占用的字节数
if err != nil {
fmt.Println("err = ", err)
return
}
fmt.Printf("[%s]: %s\n", addr, string(buf[:n]))
fmt.Println("len = ", len(string(buf[:n])))
if "exit" == string(buf[:n-1]) { //nc测试
//if "exit" == string(buf[:n-2]) { //自己写的客户端测试, 发送时,多了2个字符, "\r\n"
fmt.Println(addr, " exit")
return
}
//把数据转换为大写,再给用户发送
conn.Write([]byte(strings.ToUpper(string(buf[:n])))) //strings.ToUpper 将字符串转换为全大写
}
}
func main() {
//监听
listener, err := net.Listen("tcp", "127.0.0.1:8001") //本机的服务器的IP及port可以固定
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) //给每一个用户请求一个协程去处理(协程的好处出现了)
}
}
执行结果:使用NetCat 进行测试
Client 客户端
package main
import (
"fmt"
"net"
"os"
)
func main() {
//主动连接服务器
conn, err := net.Dial("tcp", "127.0.0.1:8001")
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) //从键盘读取内容, 放在str //os.Stdin 输入流
if err != nil {
fmt.Println("os.Stdin. 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])) //打印接收到的内容, 转换为字符串再打印
}
}
注意更改服务端判断退出的代码
执行结果
文件传输
原理分析
获取文件属性
package main
import (
"fmt"
"os"
)
func main() {
list := os.Args //第一个参数是程序路径本身 ,第二个是文件的路径(文件名(相对路径))
if len(list) != 2 {
fmt.Println("useage: xxx file")
return
}
fileName := list[1] //获取传递来的文件名(或者路径)
info, err := os.Stat(fileName) //返回该文件的一系列参数
if err != nil { //要是有错误就打印什么错
fmt.Println("err = ", err)
return
}
//打印文件名
fmt.Println("name = ", info.Name())
//打印文件大小
fmt.Println("size = ", info.Size())
}
运行结果
传输文件发送方
package main
import (
"fmt"
"io"
"net"
"os"
)
//发送文件内容
func SendFile(path string, conn net.Conn) {
//以只读方式打开文件
f, err := os.Open(path) //使用Open函数打开该路径文件
if err != nil {
fmt.Println("os.Open err = ", err)
return
}
defer f.Close() //最后要关闭
buf := make([]byte, 1024*4) //缓冲区设置为4K大小
//读文件内容,读多少发多少,一点不差
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) //获取该文件的众多参数
if err != nil {
fmt.Println("os.Stat err = ", err)
return
}
//主动连接服务器
conn, err1 := net.Dial("tcp", "127.0.0.1:8001")
if err1 != nil {
fmt.Println("net.Dial err1 = ", 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 //提前声明n ,后面自动推导也是可以的
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) //调用发送文件的函数,传递的是参数是路径以及连接的服务器
}
}
传输文件接收方
package main
import (
"fmt"
"io"
"net"
"os"
)
//接收文件内容
func RecvFile(fileName string, conn net.Conn) {
//新建文件
f, err := os.Create(fileName)
defer f.Close()
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() {
//监听
listenner, err := net.Listen("tcp", "127.0.0.1:8001")
if err != nil {
fmt.Println("net.Listen err = ", err)
return
}
defer listenner.Close()
//阻塞等待用户连接
conn, err1 := listenner.Accept()
if err1 != nil {
fmt.Println("listenner.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 = ", err)
return
}
fileName := string(buf[:n])
//回复"ok"
conn.Write([]byte("ok"))
//接收文件内容
RecvFile(fileName, 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 //声明键值对字典用来保存在线用户,下面需要使用make进行初始化
var messaage = make(chan string) ////存储转发信息的通道
//新开一个协程,转发消息,只要有消息来了,遍历map, 给map每个成员都发送此消息
func Manager() {
//给map分配空间,只是声明只有 nil 必须初始化才能使用
onlineMap = make(map[string]Client) //因为这个协程是最早开启的,只会分配一次的map空间,只声明是不能使用的
for {
msg := <-messaage //没有消息前,这里会阻塞
//遍历map, 给map每个成员都发送此消息
for _, cli := range onlineMap {
cli.C <- msg
}
}
}
func WriteMsgToClient(cli Client, conn net.Conn) {
for msg := range cli.C { //给当前客户端发送信息,cli.C是通道,只有有数据才行写入
conn.Write([]byte(msg + "\n"))
}
}
// 第7步 广播信息的创建
func MakeMsg(cli Client, msg string) (buf string) {
buf = "[" + cli.Addr + "]" + cli.Name + ": " + msg
return
}
// 第5步,进行连接的处理
func HandleConn(conn net.Conn) { //处理用户链接
defer conn.Close()
//获取客户端的网络地址
cliAddr := conn.RemoteAddr().String()
//创建一个结构体, 默认,用户名和网络地址一样
cli := Client{make(chan string), cliAddr, cliAddr}
//把结构体添加到map
onlineMap[cliAddr] = cli //键使用网络地址作为唯一值
//新开一个协程,专门给当前客户端发送信息 第6步
go WriteMsgToClient(cli, conn)
//广播某个在线
//messaage <- "[" + cli.Addr + "]" + cli.Name + ": login"
messaage <- MakeMsg(cli, "login") //用来广播的信息
//提示,我是谁
cli.C <- MakeMsg(cli, "I am here") //写入以后给当前客户端发送消息
isQuit := make(chan bool) //对方是否主动退出
hasData := make(chan bool) //对方是否有数据发送
//新建一个协程,接收用户发送过来的数据 第8步
go func() {
buf := make([]byte, 2048) //定义一个切片用来读取数据的
for { //循环别忘记加
n, err := conn.Read(buf)
if n == 0 { //对方断开,或者,出问题
isQuit <- true //对方退出了设置一个通道的断开标志位为true
fmt.Println("conn.Read err = ", err)
return
}
// 当前客户端发送的信息需要进行转发
msg := string(buf[:n-1]) //通过windows nc测试,多一个换行
if len(msg) == 3 && msg == "who" {
//遍历map,给当前用户发送所有成员 //第10步 查询有哪些在线用户
conn.Write([]byte("user list:\n"))
for _, tmp := range onlineMap {
msg = tmp.Addr + ":" + tmp.Name + "\n"
conn.Write([]byte(msg))
}
//第11步 修改当前用户名
} 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 ok\n")) //返回是否改名成功
} else {
//转发此内容 第9步
messaage <- MakeMsg(cli, msg)
}
hasData <- true //代表有数据
}
}() //别忘了()
for {
//通过select检测channel的流动 //第12步用户主动退出
select {
case <-isQuit: //使用select进行检测,如果有退出的标志位,则删除此用户
delete(onlineMap, cliAddr) //当前用户从map移除
messaage <- MakeMsg(cli, "login out") //广播谁下线了
return
case <-hasData:
// 有数据标志位就不用管
case <-time.After(30 * time.Second): //60s后 //60s后将当前用户移除
delete(onlineMap, cliAddr) //当前用户从map移除
messaage <- MakeMsg(cli, "time out leave out") //广播谁下线了
return
}
}
}
func main() {
//监听 第1步
listener, err := net.Listen("tcp", ":8001")
if err != nil {
fmt.Println("net.Listen err = ", err)
return
}
defer listener.Close()
//新开一个协程,转发消息,只要有消息来了,遍历map, 给map每个成员都发送此消息 4
go Manager() //第4步
//主协程,循环阻塞等待用户链接 第2步
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("listener.Accept err = ", err)
continue //防止整个程序的中断
}
go HandleConn(conn) //处理用户链接 第3步
}
}
需要结合上面的原理分析详细的分析代码的实现
执行结果