赞
踩
到互联网公司实习,公司提了一个需求,处在内网的服务器运行一段时间后会出问题,经检查是UPnP失效。因此要求我设计一个测试程序实现如下功能
在内网中运行一台server,利用UPnP协议发现设备(DiscoverDevice)和添加端口映射(AddPortMapping),并监听TCP某个端口,等待外网client发送消息hello,然后在server端打印该消息。最后判断是否是UPnP导致问题还是服务本身存在问题。
程序用go语言实现
import (
"fmt"
"os"
"net"
"time"
"github.com/huin/goupnp"
"github.com/huin/goupnp/dcps/internetgateway1"
)
导入依赖包,没什么好说的。
UPnP部分
第一步 发现
func GetExternalIPAddress() {
clients, errors, err := internetgateway1.NewWANIPConnection1Clients()
extIPClients := make([]GetExternalIPAddresser, len(clients))
for i, client := range clients {
extIPClients[i] = client
}
DisplayExternalIPResults(extIPClients, errors, err)
}
利用discovered WANIPConnection services来发现设备和外部ip地址,
internetgateway1.NewWANIPConnection1Clients()这块可参考https://github.com/huin/goupnp/blob/master/dcps/internetgateway1/internetgateway1.go
发现设备是UPnP的一个阶段,具体可参考:https://blog.csdn.net/jiuaiwo1314/article/details/7656427
或者UPNP协议原理
DisplayExternalIPResults用来打印设备信息
func DisplayExternalIPResults(clients []GetExternalIPAddresser, errors []error, err error) { if err != nil { fmt.Fprintln(os.Stderr, "Error discovering service with UPnP: ", err) return } if len(errors) > 0 { fmt.Fprintf(os.Stderr, "Error discovering %d services:\n", len(errors)) for _, err := range errors { fmt.Println(" ", err) } } fmt.Fprintf(os.Stderr, "Successfully discovered %d services:\n", len(clients)) for _, client := range clients { device := &client.GetServiceClient().RootDevice.Device fmt.Fprintln(os.Stderr, " Device:", device.FriendlyName) if addr, err := client.GetExternalIPAddress(); err != nil { fmt.Fprintf(os.Stderr, " Failed to get external IP address: %v\n", err) } else { fmt.Fprintf(os.Stderr, " External IP address: %v\n", addr) } } }
第二步是描述。
通过URL,下载XML文件,并从中找到有关设备的类型,服务类型,控制URL,时间触发URL等。同样分两步,首先下载描述文件。第二步解析该XML文件
for _, client := range clients { device := &client.GetServiceClient().RootDevice.Device fmt.Fprintln(os.Stderr, " Device:", device.FriendlyName) if addr, err := client.GetExternalIPAddress(); err != nil { fmt.Fprintf(os.Stderr, " Failed to get external IP address: %v\n", err) } else { fmt.Fprintf(os.Stderr, " External IP address: %v\n", addr) } srv := client.GetServiceClient().Service fmt.Println(device.FriendlyName, " :: ", srv.String()) scpd, err := srv.RequestSCPD() if err != nil { fmt.Printf(" Error requesting service SCPD: %v\n", err) } else { fmt.Println(" Available actions:") for _, action := range scpd.Actions { fmt.Printf(" * %s\n", action.Name) for _, arg := range action.Arguments { var varDesc string if stateVar := scpd.GetStateVariable(arg.RelatedStateVariable); stateVar != nil { varDesc = fmt.Sprintf(" (%s)", stateVar.DataType.Name) } fmt.Printf(" * [%s] %s%s\n", arg.Direction, arg.Name, varDesc) } } }
调srv.RequestSCPD()完成描述阶段
UPnP的最后一步了,添加端口映射
if scpd == nil || scpd.GetAction("AddPortMapping") != nil {
err := client.AddPortMapping("", 5000, "TCP", 5001, "192.168.1.2", true, "Test port mapping", 0)
fmt.Println("AddPortMapping: ", err)
}
ip地址不一定设置成192.168.1.2,根据自己主机的ip而定,端口号也可以设置成别的端口。
socket部分
func testsocket() { server := "192.168.1.20:5001" netListen, err := net.Listen("tcp", server)//内网server监听5001端口 if err != nil{ Log("connect error: ", err) os.Exit(1) } Log("Waiting for Client ...") for{ conn, err := netListen.Accept() //等待外网client发消息 if err != nil{ Log(conn.RemoteAddr().String(), "Fatal error: ", err) continue } conn.SetReadDeadline(time.Now().Add(time.Duration(10)*time.Second)) Log(conn.RemoteAddr().String(), "connect success!") go handleConnection(conn)//处理长连接,因为程序要一直运行,反复处理打印client发送的消息 } }
长连接,在Server和Socket之间设计通讯机制,当两者之间没有信息交互时,双方便会定时发送数据包(心跳),以维持连接状态。
func handleConnection(conn net.Conn) { buffer := make([]byte, 1024) for { n, err := conn.Read(buffer) if err != nil { return } Data := buffer[:n] message := make(chan byte) go HeartBeating(conn, message, 4)心跳计时 go GravelChannel(Data, message)//检测每次Client是否有数据传来 Log(time.Now().Format("2006-01-02 15:04:05.0000000"), conn.RemoteAddr().String(), string(buffer[:n])) } defer conn.Close() }
func GravelChannel(bytes []byte, mess chan byte) { for _, v := range bytes{ mess <- v } close(mess) } func HeartBeating(conn net.Conn, bytes chan byte, timeout int) { //心跳计时,根据GravelChannel判断Client是否在设定时间内发来信息 select { case fk := <- bytes: Log(conn.RemoteAddr().String(), "heartbeat", string(fk), "times") conn.SetDeadline(time.Now().Add(time.Duration(timeout) * time.Second)) break case <- time.After(5 * time.Second): Log("conn dead now") conn.Close() } } func Log(v ...interface{}) { fmt.Println(v...) return }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。