温故而知新,可以为师矣
基于目前socket在编程中的重要地位,我打算仔细琢磨一下go的socket编程。打造一个属于自己的方便调用的go socket框架。因为毕竟自己有c的socket经验,所以我觉得应该没什么难度。
一路下来也确实没有碰到什么问题,在此就大致记录下编码过程以及新体会吧。
编写go客户端:
- 获取服务器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
}
- 数据处理
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服务端:
- 首先获取到一个监听的IP地址:
//获取服务器监听ip地址
ip, err := net.ResolveTCPAddr("", ":20003")
if err != nil {
Log("err1=%v\n", err.Error())
return
}
- 创建监听的socket
//创建一个监听的socket
conn, err := net.ListenTCP("tcp", ip)
if err != nil {
Log("err2=%v\n", err.Error())
return
}
- 服务器逻辑主循环
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())
}
实验
直接启动服务器跟客户端,发现打印如下:
客户端:
2019/05/30 16:35:44.255:connected ...
2019/05/30 16:35:44.269:send server [14]=Hi,here is glp
服务端:
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
是这个报错内容:An existing connection was forcibly closed by the remote host.
意思应该是连接被远端强制关闭,诶。。这个跟io.EOF
到底有什么区别呢。我记得我在编写c++ socket的时候好像没碰到这个。印象中强制关闭的判断应该就是判断recv(...)==0
。带着这个好奇,我于是编写了c++ socket服务器。
c++服务器 主要逻辑代码:
- 服务器主循环
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("服务器创建网络描述符 fd=%d\n", listenFd);
if (setAddrReuse(listenFd) != 0) {
Printf("setAddrReuse error %d\n", errno);
return -1;
}
Printf("服务器设置地址重用成功\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("服务器绑定地址成功\n");
if (::listen(listenFd, SOMAXCONN) != 0) {
Printf("socket listen error %d\n", errno);
return -1;
}
Printf("服务器监听成功\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);//接收网络数据
if (len == 0) {
Printf("客户端连接被主动关闭\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++服务器看打印结果
客户端打印
2019/05/30 16:30:30.380:connected ...
2019/05/30 16:30:30.393:send server [14]=Hi,here is glp
c++服务器打印
2019-05-30 16:30:20.499:服务器创建网络描述符 fd=636
2019-05-30 16:30:20.499:服务器设置地址重用成功
2019-05-30 16:30:20.499:服务器绑定地址成功
2019-05-30 16:30:20.499:服务器监听成功
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
发现跟go服务器打印的错误信息一致。。。
c++的recv
函数在这里也确实返回了-1,看来并不是所有的客户端socket close,都可以让服务器的recv得到0。看来是我自己以前没怎么关注这块只顾写逻辑去了。。go的底层socket代码应该是跟c++保持一致的。
实验3 linux服务器
上面的服务器我都是在本地windows运行的,现在尝试在linux上看看。
我把编写的go服务器代码编译发布到服务器上再看服务器打印日志:
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返回的错误提示是不同的。不过他们想表达的意思应该是一致的。
实验4 尝试读取服务器发来的数据再关闭socket
上面的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)
//新增读取服务器数据代码 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))
//新增读取服务器数据代码 end
<-time.After(time.Second)
再去请求go服务器和c++服务器
go服务器打印(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服务器打印(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++服务器打印(windows):
2019-05-30 17:06:08.737:服务器创建网络描述符 fd=644
2019-05-30 17:06:08.737:服务器设置地址重用成功
2019-05-30 17:06:08.737:服务器绑定地址成功
2019-05-30 17:06:08.737:服务器监听成功
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:客户端连接被主动关闭
经过这步实验证明确实是因为客户端没有读取服务器发来的数据就关闭导致服务器的read
或者recv
报错了。
经过调试我发现我可以通过如下步骤找到对应的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
如果想要把没有读取数据的强制关闭也要归结为close而不是error的时候,用上面的办法只能用于windows。而我需要跨平台解决方案。
在提出最后的解决方案之前,我们还有一个实验需要做一下就是:
实验5 socket出现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)
把读取和等待的代码都注释了,查看服务器打印:
go服务器打印(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服务器打印(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++服务器打印(windows):
2019-05-30 17:26:23.847:服务器创建网络描述符 fd=628
2019-05-30 17:26:23.847:服务器设置地址重用成功
2019-05-30 17:26:23.847:服务器绑定地址成功
2019-05-30 17:26:23.848:服务器监听成功
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偶尔会出现10054,但是以10053居多。而linux一直是EOF。
看来这个错误码只出现在windows平台。情况是:服务器向客户端发送数据的时候客户端已经close了。
Linux应该把这种情况当EOF处理了。
解决方案:屏蔽强制关闭的错误
有时候我们服务器需要对socket的错误的原因进行分析,但是像这种客户端直接close的,我们就不需要分析了,最好在日志层面就把它过滤掉。
但是我们又不能像上面提到的直接用常量。这样我们就没办法编译跨平台的服务器,至少要去改代码才行。而笔者想要的就是不动源码的基础上分辨是否是强制关闭。考虑良久,虽然不是很好但是比较实用的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
正常关闭.指客户端读完服务器发送的数据然后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)
表示服务器发送数据,客户端已经close,这个经过测试只有在windows上才会出现。linux试了很多遍都是返回io.EOF错误
解决办法就是客户端发送数据的时候需要wait一下,然后再close,这样close的结果就是2了
*/
conn.Status = StatusClosed
} else if err1 != nil && err1.Timeout() {
conn.Status = StatusTimeout
} else {
conn.Status = StatusError
}
return
}
go服务器完整代码:
package main
import (
"fmt"
"io"
"net"
"runtime"
"strings"
"time"
)
const (
TimeFmt = "2006/01/02 15:04:05.000" //毫秒保留3位有效数字
)
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 对方主动关闭
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
正常关闭.指客户端读完服务器发送的数据然后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)
表示服务器发送数据,客户端已经close,这个经过测试只有在windows上才会出现。linux试了很多遍都是返回io.EOF错误
解决办法就是客户端发送数据的时候需要wait一下,然后再close,这样close的结果就是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() {
//服务器例子
Log("cur goos=%s\n", runtime.GOOS)
//获取服务器监听ip地址
ip, err := net.ResolveTCPAddr("", ":20003")
if err != nil {
Log("err1=%v\n", err.Error())
return
}
//创建一个监听的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})
}
}