溫故而知新吞杭,可以為師矣
基于目前socket在編程中的重要地位争剿,我打算仔細(xì)琢磨一下go的socket編程羽利。打造一個屬于自己的方便調(diào)用的go socket框架捻勉。因為畢竟自己有c的socket經(jīng)驗,所以我覺得應(yīng)該沒什么難度虹茶。
一路下來也確實沒有碰到什么問題逝薪,在此就大致記錄下編碼過程以及新體會吧。
編寫go客戶端:
- 獲取服務(wù)器ip地址
ip, err := net.ResolveIPAddr("", "127.0.0.1")
if err != nil {
Log("err1=%v\n", err.Error())
return
}
- 建立連接
ipStr := fmt.Sprintf("%s:20003", ip.IP.String())
conn, err := net.Dial("tcp", ipStr)
if err != nil {
Log("err2=%v\n", err.Error())
return
}
- 數(shù)據(jù)處理
defer func() {
err := conn.Close()
if err != nil {
Log("err3=%v\n", err.Error())
return
}
}()
Log("connected ...\n")
hello := "Hi,here is glp"
n, err := conn.Write([]byte(hello))
if err != nil {
Log("err4=%v\n", err.Error())
return
}
Log("send server [%d]=%s\n", n, hello)
<-time.After(time.Second)
編寫go服務(wù)端:
- 首先獲取到一個監(jiān)聽的IP地址:
//獲取服務(wù)器監(jiān)聽ip地址
ip, err := net.ResolveTCPAddr("", ":20003")
if err != nil {
Log("err1=%v\n", err.Error())
return
}
- 創(chuàng)建監(jiān)聽的socket
//創(chuàng)建一個監(jiān)聽的socket
conn, err := net.ListenTCP("tcp", ip)
if err != nil {
Log("err2=%v\n", err.Error())
return
}
- 服務(wù)器邏輯主循環(huán)
clientConn, err := conn.Accept()
if err != nil {
Log("err3=%v\n", err.Error())
return
}
Log("new client conn=%v,ip=%v\n", clientConn, clientConn.RemoteAddr())
go handlerClient(&StatusConn{clientConn, StatusNormal})
- 對客戶端socket的處理邏輯主要片段
buffer := make([]byte, 1024)
err := conn.SetReadDeadline(time.Now().Add(time.Second * 10))
if err != nil {
Log("handlerClient err2=%v\n", err.Error())
conn.Status = StatusError
return
}
n, err := conn.Read(buffer)
if err != nil {
Log("handlerClient err3=%v\n", err.Error())
if err == io.EOF {
conn.Status = StatusClosed
} else {
conn.Status = StatusError
}
return
}
clientBytes := buffer[:n]
Log("read client [%d]=%s\n", n, string(clientBytes))
hello := fmt.Sprintf("from server back【go】:%s", string(clientBytes))
n, err = conn.Write([]byte(hello))
Log("send client [%d]=%s\n", n, hello)
if err != nil {
Log("handlerClient err4=%v\n", err.Error())
}
實驗
直接啟動服務(wù)器跟客戶端蝴罪,發(fā)現(xiàn)打印如下:
客戶端:
2019/05/30 16:35:44.255:connected ...
2019/05/30 16:35:44.269:send server [14]=Hi,here is glp
服務(wù)端:
2019/05/30 16:35:39.543:cur goos=windows
2019/05/30 16:35:39.560:server wait...
2019/05/30 16:35:44.255:new client conn=&{{0xc0000862c0}},ip=127.0.0.1:56795
2019/05/30 16:35:44.269:read client [14]=Hi,here is glp
2019/05/30 16:35:44.269:send client [39]=from server back【go】:Hi,here is glp
2019/05/30 16:35:45.269:handlerClient err3=read tcp 127.0.0.1:20003->127.0.0.1:56795: wsarecv: An existing connection was forcibly closed by the remote host.
2019/05/30 16:35:45.269:handlerClient Status=3
是這個報錯內(nèi)容:An existing connection was forcibly closed by the remote host.
意思應(yīng)該是連接被遠(yuǎn)端強(qiáng)制關(guān)閉董济,誒。要门。這個跟io.EOF
到底有什么區(qū)別呢虏肾。我記得我在編寫c++ socket的時候好像沒碰到這個。印象中強(qiáng)制關(guān)閉的判斷應(yīng)該就是判斷recv(...)==0
欢搜。帶著這個好奇封豪,我于是編寫了c++ socket服務(wù)器。
c++服務(wù)器 主要邏輯代碼:
- 服務(wù)器主循環(huán)
int main() {
Env env = Env();
SOCKET listenFd = -1;
listenFd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//socket 描述符
if (INVALID_SOCKET == listenFd)
{
Printf("create socket error %d\n", errno);
return -1;
}
Printf("服務(wù)器創(chuàng)建網(wǎng)絡(luò)描述符 fd=%d\n", listenFd);
if (setAddrReuse(listenFd) != 0) {
Printf("setAddrReuse error %d\n", errno);
return -1;
}
Printf("服務(wù)器設(shè)置地址重用成功\n");
struct sockaddr_in ipAddr = { 0 };
ipAddr.sin_family = AF_INET;
ipAddr.sin_port = htons(20003);
ipAddr.sin_addr.s_addr = htonl(INADDR_ANY);
if (0 != bind(listenFd, (struct sockaddr*) & ipAddr, sizeof(ipAddr)))
{
Printf("socket bind error %d\n", errno);
return -1;
}
Printf("服務(wù)器綁定地址成功\n");
if (::listen(listenFd, SOMAXCONN) != 0) {
Printf("socket listen error %d\n", errno);
return -1;
}
Printf("服務(wù)器監(jiān)聽成功\n");
sockaddr addr;
#ifdef WIN32
int len = sizeof(addr);
#else
socklen_t len = sizeof(addr);
#endif
while (true) {
SOCKET clientSocket = accept(listenFd, &addr, &len);
if (clientSocket == INVALID_SOCKET) {
Printf("socket accept error %d\n", errno);
return -1;
}
Printf("客戶端新連接:接入 fd = %d,ip = %s\n", clientSocket, GetPeerIp(clientSocket));
auto clientThread = std::thread(handlerClient, clientSocket);
clientThread.detach();
}
}
- 客戶端連接處理片段
char buf[65535] = { 0 };
int len = (int) ::recv(fd, buf, sizeof(buf), 0);//接收網(wǎng)絡(luò)數(shù)據(jù)
if (len == 0) {
Printf("客戶端連接被主動關(guān)閉\n");
return;
}
if (len == SOCKET_ERROR) {
int errorcode;
#ifdef WIN32
errorcode = GetLastError();//這里的錯誤碼炒瘟,必須用windows的GetLastError獲取吹埠。
if (errorcode != WSAEWOULDBLOCK) {
#else//Linux
errorcode = errno;
if (errorcode != EAGAIN) {
#endif
Printf("client[%d] recv err=%d\n", fd, errorcode);
return;
}
continue;
}
Printf("client[%d] recv[%d]=%s\n", fd, len, buf);
char hello[65535] = { 0 };
sprintf(hello, "from server back[c++]:%s", buf);
Printf("send to client[%d]=%s\n", fd, hello);
::send(fd, hello, strlen(hello), 0);
實驗2,用go客戶端連接這個c++服務(wù)器看打印結(jié)果
客戶端打印
2019/05/30 16:30:30.380:connected ...
2019/05/30 16:30:30.393:send server [14]=Hi,here is glp
c++服務(wù)器打印
2019-05-30 16:30:20.499:服務(wù)器創(chuàng)建網(wǎng)絡(luò)描述符 fd=636
2019-05-30 16:30:20.499:服務(wù)器設(shè)置地址重用成功
2019-05-30 16:30:20.499:服務(wù)器綁定地址成功
2019-05-30 16:30:20.499:服務(wù)器監(jiān)聽成功
2019-05-30 16:30:30.381:客戶端新連接:接入 fd = 640,ip = 127.0.0.1
2019-05-30 16:30:30.393:client[640] recv[14]=Hi,here is glp
2019-05-30 16:30:30.393:send to client[640]=from server back[c++]:Hi,here is glp
2019-05-30 16:30:31.393:client[640] recv err=10054
特地去查了下10054:
//
// MessageId: WSAECONNRESET
//
// MessageText:
//
// An existing connection was forcibly closed by the remote host.
//
#define WSAECONNRESET 10054L
發(fā)現(xiàn)跟go服務(wù)器打印的錯誤信息一致疮装。缘琅。。
c++的recv
函數(shù)在這里也確實返回了-1廓推,看來并不是所有的客戶端socket close刷袍,都可以讓服務(wù)器的recv得到0》梗看來是我自己以前沒怎么關(guān)注這塊只顧寫邏輯去了呻纹。。go的底層socket代碼應(yīng)該是跟c++保持一致的专缠。
實驗3 linux服務(wù)器
上面的服務(wù)器我都是在本地windows運行的雷酪,現(xiàn)在嘗試在linux上看看。
我把編寫的go服務(wù)器代碼編譯發(fā)布到服務(wù)器上再看服務(wù)器打印日志:
2019/05/30 16:45:00.012:cur goos=linux
2019/05/30 16:45:00.012:server wait...
2019/05/30 16:46:36.104:new client conn=&{{0xc00009a080}},ip=115.192.79.216:56919
2019/05/30 16:46:36.132:read client [14]=Hi,here is glp
2019/05/30 16:46:36.132:send client [39]=from server back【go】:Hi,here is glp
2019/05/30 16:46:37.133:handlerClient err3=read tcp 172.16.200.43:20003->115.192.79.216:56919: read: connection reset by peer
2019/05/30 16:46:37.133:handlerClient Status=3
到了linux這邊就變成了connection reset by peer
.
可以看到windows和linux返回的錯誤提示是不同的藤肢。不過他們想表達(dá)的意思應(yīng)該是一致的太闺。
實驗4 嘗試讀取服務(wù)器發(fā)來的數(shù)據(jù)再關(guān)閉socket
上面的go客戶端是沒有讀取服務(wù)器返回的數(shù)據(jù),自己等待一秒鐘就關(guān)閉了∴胰Γ現(xiàn)在把客戶端邏輯改成:
Log("connected ...\n")
hello := "Hi,here is glp"
n, err := conn.Write([]byte(hello))
if err != nil {
Log("err4=%v\n", err.Error())
return
}
Log("send server [%d]=%s\n", n, hello)
//新增讀取服務(wù)器數(shù)據(jù)代碼 begin
buffer := make([]byte, 1024)
n, err = conn.Read(buffer)
if err != nil {
Log("err5=%v\n", err.Error())
return
}
Log("read server [%d]=%s\n", n, string(buffer))
//新增讀取服務(wù)器數(shù)據(jù)代碼 end
<-time.After(time.Second)
再去請求go服務(wù)器和c++服務(wù)器
go服務(wù)器打印(linux):
2019/05/30 17:02:28.809:cur goos=linux
2019/05/30 17:02:28.809:server wait...
2019/05/30 17:02:43.239:new client conn=&{{0xc00009a080}},ip=115.192.79.216:57079
2019/05/30 17:02:43.253:read client [14]=Hi,here is glp
2019/05/30 17:02:43.253:send client [39]=from server back【go】:Hi,here is glp
2019/05/30 17:02:44.259:handlerClient err3=EOF
2019/05/30 17:02:44.259:handlerClient Status=1
go服務(wù)器打印(windows):
2019/05/30 17:03:21.963:cur goos=windows
2019/05/30 17:03:21.976:server wait...
2019/05/30 17:03:31.903:new client conn=&{{0xc00008c2c0}},ip=127.0.0.1:57090
2019/05/30 17:03:31.922:read client [14]=Hi,here is glp
2019/05/30 17:03:31.922:send client [39]=from server back【go】:Hi,here is glp
2019/05/30 17:03:32.923:handlerClient err3=EOF
2019/05/30 17:03:32.923:handlerClient Status=1
c++服務(wù)器打印(windows):
2019-05-30 17:06:08.737:服務(wù)器創(chuàng)建網(wǎng)絡(luò)描述符 fd=644
2019-05-30 17:06:08.737:服務(wù)器設(shè)置地址重用成功
2019-05-30 17:06:08.737:服務(wù)器綁定地址成功
2019-05-30 17:06:08.737:服務(wù)器監(jiān)聽成功
2019-05-30 17:06:29.437:客戶端新連接:接入 fd = 604,ip = 127.0.0.1
2019-05-30 17:06:29.449:client[604] recv[14]=Hi,here is glp
2019-05-30 17:06:29.450:send to client[604]=from server back[c++]:Hi,here is glp
2019-05-30 17:06:30.451:客戶端連接被主動關(guān)閉
經(jīng)過這步實驗證明確實是因為客戶端沒有讀取服務(wù)器發(fā)來的數(shù)據(jù)就關(guān)閉導(dǎo)致服務(wù)器的read
或者recv
報錯了省骂。
經(jīng)過調(diào)試我發(fā)現(xiàn)我可以通過如下步驟找到對應(yīng)的error
if err1, ok := err.(*net.OpError); ok {
if err2, ok := err1.Err.(*os.SyscallError); ok {
if err3, ok := err2.Err.(syscall.Errno); ok {
if err3 == syscall.WSAECONNRESET {
Log("err3 == syscall.WSAECONNRESET True\n")
}
}
}
}
但是
syscall.WSAECONNRESET
不能用于linux蟀淮,在編譯linux的時候會報錯:
src\study\testServer.go:68:18: undefined: syscall.WSAECONNRESET
如果想要把沒有讀取數(shù)據(jù)的強(qiáng)制關(guān)閉也要歸結(jié)為close而不是error的時候,用上面的辦法只能用于windows钞澳。而我需要跨平臺解決方案怠惶。
在提出最后的解決方案之前,我們還有一個實驗需要做一下就是:
實驗5 socket出現(xiàn)10053的原因
改寫我們的go客戶端:
Log("connected ...\n")
hello := "Hi,here is glp"
n, err := conn.Write([]byte(hello))
if err != nil {
Log("err4=%v\n", err.Error())
return
}
Log("send server [%d]=%s\n", n, hello)
//buffer := make([]byte, 1024)
//n, err = conn.Read(buffer)
//if err != nil {
// Log("err5=%v\n", err.Error())
// return
//}
//Log("read server [%d]=%s\n", n, string(buffer))
//
//<-time.After(time.Second)
把讀取和等待的代碼都注釋了轧粟,查看服務(wù)器打硬咧巍:
go服務(wù)器打印(linux):
2019/05/30 17:26:09.186:cur goos=linux
2019/05/30 17:26:09.187:server wait...
2019/05/30 17:26:12.750:new client conn=&{{0xc0000b4080}},ip=115.192.79.216:57420
2019/05/30 17:26:12.760:read client [14]=Hi,here is glp
2019/05/30 17:26:12.760:send client [39]=from server back【go】:Hi,here is glp
2019/05/30 17:26:12.760:handlerClient err3=EOF
2019/05/30 17:26:12.760:handlerClient Status=1
go服務(wù)器打印(windows):
2019/05/30 17:25:42.506:cur goos=windows
2019/05/30 17:25:42.520:server wait...
2019/05/30 17:25:48.386:new client conn=&{{0xc00008c2c0}},ip=127.0.0.1:57409
2019/05/30 17:25:48.400:read client [14]=Hi,here is glp
2019/05/30 17:25:48.400:send client [39]=from server back【go】:Hi,here is glp
2019/05/30 17:25:48.400:handlerClient err3=read tcp 127.0.0.1:20003->127.0.0.1:57409: wsarecv: An established connection was aborted by the software in your host machine.
2019/05/30 17:25:48.400:handlerClient Status=3
c++服務(wù)器打印(windows):
2019-05-30 17:26:23.847:服務(wù)器創(chuàng)建網(wǎng)絡(luò)描述符 fd=628
2019-05-30 17:26:23.847:服務(wù)器設(shè)置地址重用成功
2019-05-30 17:26:23.847:服務(wù)器綁定地址成功
2019-05-30 17:26:23.848:服務(wù)器監(jiān)聽成功
2019-05-30 17:26:53.961:客戶端新連接:接入 fd = 640,ip = 127.0.0.1
2019-05-30 17:26:53.973:client[640] recv[14]=Hi,here is glp
2019-05-30 17:26:53.973:send to client[640]=from server back[c++]:Hi,here is glp
2019-05-30 17:26:53.973:client[640] recv err=10053
筆者多實驗了幾次,windows偶爾會出現(xiàn)10054兰吟,但是以10053居多通惫。而linux一直是EOF。
看來這個錯誤碼只出現(xiàn)在windows平臺混蔼。情況是:服務(wù)器向客戶端發(fā)送數(shù)據(jù)的時候客戶端已經(jīng)close了履腋。
Linux應(yīng)該把這種情況當(dāng)EOF處理了。
解決方案:屏蔽強(qiáng)制關(guān)閉的錯誤
有時候我們服務(wù)器需要對socket的錯誤的原因進(jìn)行分析惭嚣,但是像這種客戶端直接close的遵湖,我們就不需要分析了,最好在日志層面就把它過濾掉晚吞。
但是我們又不能像上面提到的直接用常量延旧。這樣我們就沒辦法編譯跨平臺的服務(wù)器,至少要去改代碼才行槽地。而筆者想要的就是不動源碼的基礎(chǔ)上分辨是否是強(qiáng)制關(guān)閉迁沫。考慮良久闷盔,雖然不是很好但是比較實用的go代碼:
n, err := conn.Read(buffer)
if err != nil {
Log("handlerClient err3=%v\n", err.Error())
errStr := err.Error()
err1 := err.(*net.OpError)
if err == io.EOF || (runtime.GOOS == "windows" &&
strings.Contains(errStr, "An existing connection was forcibly closed by the remote host") ||
strings.Contains(errStr, "An established connection was aborted by the software in your host machine")) ||
strings.Contains(errStr, "connection reset by peer") {
/*
1.io.EOF
正常關(guān)閉.指客戶端讀完服務(wù)器發(fā)送的數(shù)據(jù)然后close
2.
connection reset by peer(linux)
An existing connection was forcibly closed by the remote host(windows)
表示客戶端 【沒有讀取/讀取部分】就close
3.An established connection was aborted by the software in your host machine(windows)
表示服務(wù)器發(fā)送數(shù)據(jù)弯洗,客戶端已經(jīng)close,這個經(jīng)過測試只有在windows上才會出現(xiàn)。linux試了很多遍都是返回io.EOF錯誤
解決辦法就是客戶端發(fā)送數(shù)據(jù)的時候需要wait一下逢勾,然后再close,這樣close的結(jié)果就是2了
*/
conn.Status = StatusClosed
} else if err1 != nil && err1.Timeout() {
conn.Status = StatusTimeout
} else {
conn.Status = StatusError
}
return
}
go服務(wù)器完整代碼:
package main
import (
"fmt"
"io"
"net"
"runtime"
"strings"
"time"
)
const (
TimeFmt = "2006/01/02 15:04:05.000" //毫秒保留3位有效數(shù)字
)
func Log(format string, args ...interface{}) {
fmt.Printf(fmt.Sprintf("%s:%s", time.Now().Format(TimeFmt), format), args...)
}
type StatusNO int32
const (
StatusNormal StatusNO = iota //0 正常
StatusClosed //1 對方主動關(guān)閉
StatusTimeout //2 超時連接斷開
StatusError //3 其他異常斷開
)
type StatusConn struct {
net.Conn
//StatusNormal
Status StatusNO
}
func handlerClient(conn *StatusConn) {
defer func() {
Log("handlerClient Status=%v\n", conn.Status)
err := conn.Close()
if err != nil {
Log("handlerClient err1=%v\n", err)
}
}()
for {
buffer := make([]byte, 1024)
err := conn.SetReadDeadline(time.Now().Add(time.Second * 10))
if err != nil {
Log("handlerClient err2=%v\n", err.Error())
conn.Status = StatusError
return
}
n, err := conn.Read(buffer)
if err != nil {
Log("handlerClient err3=%v\n", err.Error())
errStr := err.Error()
err1 := err.(*net.OpError)
if err == io.EOF || (runtime.GOOS == "windows" &&
strings.Contains(errStr, "An existing connection was forcibly closed by the remote host") ||
strings.Contains(errStr, "An established connection was aborted by the software in your host machine")) ||
strings.Contains(errStr, "connection reset by peer") {
/*
1.io.EOF
正常關(guān)閉.指客戶端讀完服務(wù)器發(fā)送的數(shù)據(jù)然后close
2.
connection reset by peer(linux)
An existing connection was forcibly closed by the remote host(windows)
表示客戶端 【沒有讀取/讀取部分】就close
3.An established connection was aborted by the software in your host machine(windows)
表示服務(wù)器發(fā)送數(shù)據(jù)藐吮,客戶端已經(jīng)close,這個經(jīng)過測試只有在windows上才會出現(xiàn)溺拱。linux試了很多遍都是返回io.EOF錯誤
解決辦法就是客戶端發(fā)送數(shù)據(jù)的時候需要wait一下,然后再close谣辞,這樣close的結(jié)果就是2了
*/
conn.Status = StatusClosed
} else if err1 != nil && err1.Timeout() {
conn.Status = StatusTimeout
} else {
conn.Status = StatusError
}
return
}
clientBytes := buffer[:n]
Log("read client [%d]=%s\n", n, string(clientBytes))
hello := fmt.Sprintf("from server back【go】:%s", string(clientBytes))
n, err = conn.Write([]byte(hello))
Log("send client [%d]=%s\n", n, hello)
if err != nil {
Log("handlerClient err4=%v\n", err.Error())
}
}
}
func main() {
//服務(wù)器例子
Log("cur goos=%s\n", runtime.GOOS)
//獲取服務(wù)器監(jiān)聽ip地址
ip, err := net.ResolveTCPAddr("", ":20003")
if err != nil {
Log("err1=%v\n", err.Error())
return
}
//創(chuàng)建一個監(jiān)聽的socket
conn, err := net.ListenTCP("tcp", ip)
if err != nil {
Log("err2=%v\n", err.Error())
return
}
Log("server wait...\n")
for {
clientConn, err := conn.Accept()
if err != nil {
Log("err3=%v\n", err.Error())
return
}
Log("new client conn=%v,ip=%v\n", clientConn, clientConn.RemoteAddr())
go handlerClient(&StatusConn{clientConn, StatusNormal})
}
}