github.com/nxtrace/NTrace-core@v1.3.1-0.20240513132635-39169291e8c9/ipgeo/leo.go (about)

     1  package ipgeo
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"strconv"
     7  	"sync"
     8  	"time"
     9  
    10  	"github.com/nxtrace/NTrace-core/wshandle"
    11  	"github.com/tidwall/gjson"
    12  )
    13  
    14  /***
    15   * 原理介绍 By Leo
    16   * WebSocket 一共开启了一个发送和一个接收协程,在 New 了一个连接的实例对象后,不给予关闭,持续化连接
    17   * 当有新的IP请求时,一直在等待IP数据的发送协程接收到从 leo.go 的 sendIPRequest 函数发来的IP数据,向服务端发送数据
    18   * 由于实际使用时有大量并发,但是 ws 在同一时刻每次有且只能处理一次发送一条数据,所以必须给 ws 连接上互斥锁,保证每次只有一个协程访问
    19   * 运作模型可以理解为一个 Node 一直在等待数据,当获得一个新的任务后,转交给下一个协程,不再关注这个 Node 的下一步处理过程,并且回到空闲状态继续等待新的任务
    20  ***/
    21  
    22  // IPPool IP 查询池 map - ip - ip channel
    23  type IPPool struct {
    24  	pool    map[string]chan IPGeoData
    25  	poolMux sync.Mutex
    26  }
    27  
    28  var IPPools = IPPool{
    29  	pool: make(map[string]chan IPGeoData),
    30  }
    31  
    32  func sendIPRequest(ip string) {
    33  	wsConn := wshandle.GetWsConn()
    34  	wsConn.MsgSendCh <- ip
    35  }
    36  
    37  func receiveParse() {
    38  	// 获得连接实例
    39  	wsConn := wshandle.GetWsConn()
    40  	// 防止多协程抢夺一个ws连接,导致死锁,当一个协程获得ws的控制权后上锁
    41  	wsConn.ConnMux.Lock()
    42  	// 函数退出时解锁,给其他协程使用
    43  	defer wsConn.ConnMux.Unlock()
    44  	for {
    45  		// 接收到了一条IP信息
    46  		data := <-wsConn.MsgReceiveCh
    47  
    48  		// json解析 -> data
    49  		res := gjson.Parse(data)
    50  		// 根据返回的IP信息,发送给对应等待回复的IP通道上
    51  		var domain = res.Get("domain").String()
    52  
    53  		if res.Get("domain").String() == "" {
    54  			domain = res.Get("owner").String()
    55  		}
    56  
    57  		m := make(map[string][]string)
    58  		err := json.Unmarshal([]byte(res.Get("router").String()), &m)
    59  		if err != nil {
    60  			// 此处是正常的,因为有些IP没有路由信息
    61  		}
    62  
    63  		lat, _ := strconv.ParseFloat(res.Get("lat").String(), 32)
    64  		lng, _ := strconv.ParseFloat(res.Get("lng").String(), 32)
    65  
    66  		IPPools.pool[gjson.Parse(data).Get("ip").String()] <- IPGeoData{
    67  			Asnumber:  res.Get("asnumber").String(),
    68  			Country:   res.Get("country").String(),
    69  			CountryEn: res.Get("country_en").String(),
    70  			Prov:      res.Get("prov").String(),
    71  			ProvEn:    res.Get("prov_en").String(),
    72  			City:      res.Get("city").String(),
    73  			CityEn:    res.Get("city_en").String(),
    74  			District:  res.Get("district").String(),
    75  			Owner:     domain,
    76  			Lat:       lat,
    77  			Lng:       lng,
    78  			Isp:       res.Get("isp").String(),
    79  			Whois:     res.Get("whois").String(),
    80  			Prefix:    res.Get("prefix").String(),
    81  			Router:    m,
    82  		}
    83  	}
    84  }
    85  
    86  func LeoIP(ip string, timeout time.Duration, lang string, maptrace bool) (*IPGeoData, error) {
    87  	// TODO: 根据lang的值请求中文/英文API
    88  	// TODO: 根据maptrace的值决定是否请求经纬度信息
    89  	if timeout < 5*time.Second {
    90  		timeout = 5 * time.Second
    91  	}
    92  
    93  	// 缓存中没有找到IP信息,需要请求API获取
    94  	IPPools.poolMux.Lock()
    95  	// 如果之前已经被别的协程初始化过了就不用初始化了
    96  	if IPPools.pool[ip] == nil {
    97  		IPPools.pool[ip] = make(chan IPGeoData)
    98  	}
    99  	IPPools.poolMux.Unlock()
   100  	// 发送请求
   101  	sendIPRequest(ip)
   102  	// 同步开启监听
   103  	go receiveParse()
   104  
   105  	// 拥塞,等待数据返回
   106  	select {
   107  	case res := <-IPPools.pool[ip]:
   108  		return &res, nil
   109  	// 5秒后依旧没有接收到返回的IP数据,不再等待,超时异常处理
   110  	case <-time.After(timeout):
   111  		// 这里不可以返回一个 nil,否则在访问对象内部的键值的时候会报空指针的 Fatal Error
   112  		return &IPGeoData{}, errors.New("TimeOut")
   113  	}
   114  }