心跳包的实现思路:
客户端连接上服务端后,在服务端会维护一个在线客户端列表。客户端每隔一段时间,向服务端发送一个心跳包,服务端受收到包以后,会更新客户端最近一次在线时间。一旦服务端超过规定时间没有接收到客户端发来的包,则视为掉线。
代码:
客户端每隔一段时间,发送一个心跳包:
var timer = new System.Timers.Timer();
timer.Interval = 60000; //1m触发一次
timer.Start();
timer.Elapsed += (sender, args) =>
{
Console.WriteLine("开始发送心跳包");
MMessage message = new MMessage();
message.MessageType = MessagePicks.Heartbeat;
// message.From = loginName;
WriteToStream(message);
};
服务端每隔一段时间检测:
var timer = new Timer();
timer.Interval = 60000; //1m触发一次
timer.Start();
timer.Elapsed += (sender, args) =>
{
List<MClient> offClients = new List<MClient>();
foreach (var client in clients)
{
if ((DateTime.Now - client.LastOnLine).TotalMinutes > 1)
{
Console.WriteLine("用户" + client.Name + "掉线!");
offClients.Add(client);
}
}
foreach (var offClient in offClients)
{
clients.Remove(offClient);
}
};
服务端收到心跳包的处理逻辑:
Console.WriteLine("收到客户端" + msg.RemoteEndPoint + "的心跳回应包.");
client.LastOnLine = DateTime.Now; //收到心跳包,更新时间
client.Name = msg.RemoteEndPoint.ToString();
client.RemoteEndPoint = msg.RemoteEndPoint;
if (!clients.Contains(client))
{
clients.Add(client);
}
效果:
Winsock 是 Windows下套接字标准
Socket套接字基于计算机网络,提供同一系统上不同进程或由局域网连接在一起的不同机器上的进程间通讯功能。如下图:
套接字通过IP地址,Port端口号标识,通过这个标识可以在整个局域网定位一个套接字,通过套接字进程便可以相互传输数据。如:进程A与进程B之间欲通过套接字通信,首先进程A创建一个有IP地址,端口号唯一标识的套接字,进程B同样创建一个有IP地址,端口号唯一标识的套接字,进程A,B便可以通过对方套接字发送与接收信息。
TCP提供是可靠的数据传输服务,通过TCP套接口函数使用。
1.库支持:
Winsock API 函数由WS2_32.DLL支持,可通过WS2_32.LIB访问。Windows socket编程前需要初始化WS2_32.DLL,通过han函数WSAStartup完成初始化。
#include <winsock2.h>
#pragma comment(lib,”ws2_32.lib”) //静态加载ws2_32.lib
WS2_32.DLL 初始化:
int WSAStartup(WORD wVersionRequested,LPWSADATA lpWSAData);
该函数第一个参数为WS2_32.DLL版本,通常设为MAKEWORD(2,0),第二个参数为一个指向WSADATA的指针,用于返回WS2_32.DLL的配置信息。Socket编程前需要调用该函数。
WS2_32.DLL 释放:
int WSACleanup( );
该函数用于释放WS2_32.DLL,不需要Winsock功能时调用该函数。
2.TCP基本函数:
创建socket:
SOCKET socket (int af, int type, int protocol);
第一个参数af: 表示族地址,网络编程一般使用AF_INET宏。
第二个参数type: 表示连接类型,TCP选面向连接SOCK_STREAM,UDP选数据报SOCK_DGRAM。
第三个参数protocol: 表示协议,使用AF_INET族地址TCP连接时,设为IPPROTO_TCP。
绑定socket:
int bind(SOCKET s, const struct sockaddr *saddr,int namelen);
第一个参数: 需要绑定的socket。
第二个参数: 对应AF_INET,使用struct sockaddr_in包含协议,IP,端口等信息。
第三个参数: 对应AF_INET,使用struct sockaddr_in结构大小。
该函数将socket与协议,IP,端口号绑定起来,相对于给socket“命名”唯一的标识,这样其他的进程就可以通过这个标识找到这个socket。
监听socket:
int listen(SOCKET s, int nQueueSize);
第一个参数: 监听的socket。
第二个参数: 套接字监听队列最大连接请求数。
该函数将监听对socket的连接请求。
请求连接:
int connect(SOCKET s,const struct sockaddr *saddr,int namelen) ;
第一个参数: socket本地进程的socket。
第二个参数: 对应AF_INET,对方IP,端口等socket地址标识sockaddr_in。
第三个参数: 对应AF_INET,使用sockaddr_in结构大小。
接受连接请求:
SOCKET accept(SOCKET s, struct sockaddr *addr, int *addrlen);
第一个参数: socket为被监听的socket。
第二个参数: 对应AF_INET,一个sockaddr指针,将写入发送请求方的sockaddr_in信息。
第三个参数: 对应AF_INET,sockaddr_in结构体的大小。
该函数用于接受一个socket连接请求,返回一个新的连接socket(实质是请求方的socket),发送与接收数据通过这个连接套socket。
发送数据:
int send(SOCKET s, const char *buf, int len, int flags );
第一个参数: socket为对方的socket。
第二个参数: 发送数据的缓冲区。
第三个参数: 数据缓冲区大小。
第四个参数: 紧急状态,一般这为0。
该函数用于向对方socket发送数据,成功返回发送数据的大小数。
发送数据:
int recv(SOCKET s, char *buf, int len, int flags);
第一个参数: socket为对方的socket。
第二个参数: 接收数据的缓冲区。
第三个参数: 缓冲区大小。
第四个参数: 紧急状态,一般这为0。
该函数用于接收对方发送的数据,成功返回发送数据的大小数。
关闭套接字:
int closesocket(SOCKET s);
参数为socket。
该函数用于关闭套接字。
3.牛刀小试:
VC6.0中,先运行TCP服务器程序,再打开一个VC6.0,运行TCP客户端程序。
运行效果:
服务器端程序:
#include
#include
#pragma comment(lib,"ws2_32.lib")
int main()
{
SOCKET serversoc;
SOCKET clientsoc;
SOCKADDR_IN serveraddr;
SOCKADDR_IN clientaddr;
char buf[1024];
int len;
WSADATA wsa;
WSAStartup(MAKEWORD(2,0),&wsa); //初始化WS2_32.DLL
//创建套接字
if((serversoc = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) <= 0)
{
printf("套接字socket创建失败!\n");
return -1;
}
//命名协议,IP,端口
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(9102);
serveraddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
//绑定套接字
if(bind(serversoc, (SOCKADDR *)&serveraddr, sizeof(serveraddr)) != 0)
{
printf("套接字绑定失败!\n");
return -1;
}
printf("开始监听...\n");
//监听请求
if(listen(serversoc, 1) != 0)
{
printf("监听失败!\n");
return -1;
}
len = sizeof(SOCKADDR_IN);
//接收请求
if((clientsoc = accept(serversoc, (SOCKADDR *)&clientaddr, &len))<=0)
{
printf("接受连接失败!\n");
return -1;
}
printf("连接成功\n");
//接收数据
while(1)
{
if(recv(clientsoc, buf, 1024, 0) <= 0)
{
printf("关闭连接!\n");
closesocket(clientsoc);
}
printf("接收来自客户端的信息: %s\n",buf);
break;
}
//发送数据
printf("请输入发送给客户端的字符:\n");
scanf("%s", buf);
//send to client
if(send(clientsoc, buf, strlen(buf)+1, 0)<=0)
{
printf("发送错误!\n");
}
WSACleanup(); //释放WS2_32.DLL
return 0;
}
客户端程序:
#include
#include
#pragma comment(lib,"ws2_32.lib")
int main()
{
SOCKET clientsocket;
SOCKADDR_IN serveraddr;
SOCKADDR_IN clientaddr;
char buf[1024];
WSADATA wsa;
WSAStartup(MAKEWORD(2,0),&wsa); //初始化WS2_32.DLL
//创建套接字
if((clientsocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) <= 0)
{
printf("套接字socket创建失败!\n");
return -1;
}
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(9102);
serveraddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
//请求连接
printf("尝试连接中...\n");
if(connect(clientsocket, (SOCKADDR *)&serveraddr, sizeof(serveraddr)) != 0)
{
printf("连接失败!\n");
return -1;
}
printf("连接成功!\n");
//发送数据
printf("请输入发送给服务器的字符:\n");
scanf("%s", buf);
if(send(clientsocket, buf, strlen(buf)+1, 0)<=0)
{
printf("发送错误!\n");
}
//接收数据
while(1){
if(recv(clientsocket, buf, 1024, 0) <= 0)
{
printf("关闭连接!\n");
closesocket(clientsocket);
}
printf("接收来自服务器的信息: %s\n",buf);
break;
}
//关闭套接字
closesocket(clientsocket);
WSACleanup(); //释放WS2_32.DLL
return 0;
}