利用I/O复用模型实现一个时间同步服务器
一、实验内容
- 服务端采用I/O复用模型(select函数)接收客户端的时间同步请求
- 服务端采用单线程,但要能同时接收多客户端的连接请 求,显示客户端IP和端口,并向其回送时间信息。
- 客户端尝试同时使用 UDP 和 TCP 来实现。
- 注:借助 I/O 复用模型,用单线程达到多线程的效果。
二、实验要求
提交源码(源码编写要规范)、可执行程序、实验报告(要有程序运行截图)。
三、实验分析
select 介绍
select函数
的作用是监听指定的多个I/O的文件描述符,在设定的时间内阻塞,当有一个或者多个I/O端口满足某个“读”或者“写”的条件,则在fd_set
类型参数中标记并返回。
fd_set类型
是一个默认大小为1024位的类型,每一位代表一个I/0文件描述符。例如每个进程默认0、1、2
分别表示标准输入、标准输出和标准错误输出,所以fd_set类型
1024位中0、1、2位
分别代表标准输入、标准输出和标准错误输出。同理假如进程里创建了一个监听socket文件描述符为3,则fd_set的第3位代表这个监听sockfd;假如用accept
了客户端连接返回一个fdSocket为n,那么fd_set
的第n个位就代表这个fdSocket。(因为在一个进程里,某个I/O口的文件描述符是唯一的。)
fd_set fdRead,fdSocket;
FD_ZERO (&fdSocket);//将fdset所有“位”置0
FD_SET(TcpServerSocket,&fdSocket);
FD_SET(UdpServerSocket,&fdSocket);
server
#include <iostream>
#include <WinSock2.h>
#include <string>
#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
using namespace std;
#pragma comment (lib,"ws2_32.lib")
#define BUFFER_LEN 1024
#define IP_ADDRESS "127.0.0.1"
#define PORT 8888
int main()
{
WSADATA Ws;
WORD sockVersion = MAKEWORD(2,2);
SOCKET TcpServerSocket = INVALID_SOCKET;
SOCKET UdpServerSocket = INVALID_SOCKET;
SOCKET ConnectSocket = INVALID_SOCKET;
if ( WSAStartup(sockVersion, &Ws) != 0 )
{
cout<<"Init Windows Socket Failed:"<<GetLastError()<<endl;
closesocket(ConnectSocket);
return -1;
}
sockaddr_in ClientAddr;
char recvbuf[BUFFER_LEN];
char sendbuf[BUFFER_LEN];
int iResult;
// 创建用于监听的套接字
TcpServerSocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if ( TcpServerSocket == INVALID_SOCKET )
{
cout<<"Create Socket Failed:"<<GetLastError()<<endl;
closesocket(TcpServerSocket);
WSACleanup();
return -1;
}
// 为套接字绑定地址和端口号
// 监听端口为DEFAULT_PORT
struct sockaddr_in serAddr;
serAddr.sin_family = AF_INET;
serAddr.sin_port = htons(PORT);
serAddr.sin_addr.s_addr = inet_addr(IP_ADDRESS);
memset(serAddr.sin_zero,0x00,8);
int Addrlen = sizeof(serAddr);
SYSTEMTIME time;
GetLocalTime(&time);
iResult = bind(TcpServerSocket, (struct sockaddr*)&serAddr, sizeof(serAddr));
if(iResult == SOCKET_ERROR)
{
cout<<"Bind Error !"<<endl;
return -1;
}
sockaddr_in remoteAddr;
int nAddrLen = sizeof(remoteAddr);
UdpServerSocket = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
iResult = bind(UdpServerSocket,(struct sockaddr *)&serAddr,sizeof(serAddr));
if(iResult == SOCKET_ERROR)
{
cout<<"Bind Error !"<<endl;
return -1;
}
// 监听套接字
iResult = listen(TcpServerSocket,SOMAXCONN);
if(iResult == SOCKET_ERROR)
{
cout<<"---Listen Failed ---!"<<endl;
closesocket(TcpServerSocket);
WSACleanup();
return -1;
}
cout<<"---Server Starting---"<<endl;
fd_set fdRead,fdSocket;
FD_ZERO (&fdSocket);
FD_SET(TcpServerSocket,&fdSocket);
FD_SET(UdpServerSocket,&fdSocket);
while(true)
{
//通过select等待数据到达事件,如果有事件发生
//select函数移除fdRead集合中没有未决I/O操作的套接字句柄,然后返回
//cout<<1<<endl;
fdRead = fdSocket;
iResult = select(0, &fdRead, NULL, NULL, NULL);
//cout<<2<<endl;
if(iResult>0)
{
//有网络事件发生
//确定有哪些套接字有未决的I/O,并进一步处理这些I/O
for (int i = 0; i<(int)fdSocket.fd_count; i++)
{
if(FD_ISSET(fdSocket.fd_array[i], &fdRead))
{
if(fdSocket.fd_array[i] == TcpServerSocket)
{
if(fdSocket.fd_count < FD_SETSIZE)
{
//同时复用的套接字数量不能大于FD_SETSIZE
//有新的连接请求
ConnectSocket = accept(TcpServerSocket,(struct sockaddr*)&ClientAddr, &Addrlen);
if (ConnectSocket == INVALID_SOCKET)
{
cout<<"---Accept Failed---"<<endl;
closesocket(TcpServerSocket);
WSACleanup();
return 1;
}
//增加新的连接套接字进行复用等待
FD_SET(ConnectSocket, &fdSocket);
cout<<"New Connection "<<inet_ntoa(ClientAddr.sin_addr)<<":"<<ClientAddr.sin_port<<endl;
}
else
{
cout<<"No Enough Socket"<<endl;
}
}
else if(fdSocket.fd_array[i]==UdpServerSocket)
{
memset(recvbuf,0,sizeof(recvbuf));
iResult = recvfrom (fdSocket.fd_array[i],recvbuf,sizeof(recvbuf),0,(sockaddr *)&remoteAddr,&nAddrLen);
if(iResult > 0)
{
cout<<"UDP Connection!"<<endl;
cout<<"Connection "<<inet_ntoa(remoteAddr.sin_addr)<<":"<<remoteAddr.sin_port<<endl;
SYSTEMTIME time;
GetLocalTime(&time);
cout<<"Linking Time:"<<time.wYear<<"/"<<time.wMonth<<"/"<<time.wDay<<" "<<time.wHour<<":"<<time.wMinute<<"'"<<time.wSecond<<endl;
int ret = sendto(fdSocket.fd_array[i],(char *)&time,sizeof(time),0,(sockaddr *)&remoteAddr,nAddrLen);
if(ret > 0)
{
cout<<"Send Time Success!"<<endl;
cout<<"-----------------------------"<<endl;
}
else
{
cout<<"Send Time Error!"<<endl;
}
}
}
else
{
memset(recvbuf, 0, BUFFER_LEN);
iResult = recv(fdSocket.fd_array[i], recvbuf, BUFFER_LEN, 0);
if(iResult >0)
{
cout<<"TCP Connection!"<<endl;
cout<<"Connection "<<inet_ntoa(ClientAddr.sin_addr)<<":"<<ClientAddr.sin_port<<endl;
//回送时间
SYSTEMTIME time;
GetLocalTime(&time);
cout<<"Linking Time:"<<time.wYear<<"/"<<time.wMonth<<"/"<<time.wDay<<" the day of week is "<<time.wDayOfWeek<<" "<<time.wHour<<":"<<time.wMinute<<"'"<<time.wSecond<<"''"<<time.wMilliseconds<<endl;
int ret = send(fdSocket.fd_array[i], (char *)&time, sizeof(time), 0);
if(ret > 0)
{
cout<<"Send Time Success!"<<endl;
}
else
{
cout<<"Send Time Error!"<<endl;
}
}
else if(iResult == 0)
{
cout<<"Current Connection closing..."<<endl;
cout<<"-----------------------------"<<endl;
closesocket(fdSocket.fd_array[i]);
FD_CLR(fdSocket.fd_array[i], &fdSocket);
}
else
{
//情况2:连接关闭
cout<<"Receive Failed With Error"<<endl;
closesocket(fdSocket.fd_array[i]);
FD_CLR(fdSocket.fd_array[i], &fdSocket);
}
}
}
}
}
else
{
cout<<"Select Failed With Error:"<<WSAGetLastError()<<endl;
break;
}
}
closesocket(TcpServerSocket);
closesocket(UdpServerSocket);
WSACleanup();
return 0;
}
io_udpclient
#include <iostream>
#include <stdio.h>
#include <winsock2.h>
#include <string>
#include <Windows.h>
using namespace std;
#pragma comment(lib, "ws2_32.lib")
#define BUFFER_LEN 1024
int main()
{
WSADATA Ws;
SOCKET ClientSocket;
//¼ÓÔØSocket¿â
char SendBuffer[BUFFER_LEN];
int iResult;
char IP_ADDRESS[15];
unsigned short int PORT;
if ( WSAStartup(MAKEWORD(2,2), &Ws) != 0 )
{
cout<<"Init Windows Socket Failed::"<<GetLastError()<<endl;
return -1;
}
//Create Socket
ClientSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if ( ClientSocket == INVALID_SOCKET )
{
cout<<"Create Socket Failed::"<<GetLastError()<<endl;
return -1;
}
cout<<"Input Server IP:"<<endl;
cin>>IP_ADDRESS;
cout<<"Input Server PORT:"<<endl;
cin>>PORT;
struct sockaddr_in ServerAddr;
ServerAddr.sin_family = AF_INET;
ServerAddr.sin_addr.s_addr = inet_addr(IP_ADDRESS);
ServerAddr.sin_port = htons(PORT);
int len = sizeof(ServerAddr);
//memset(ServerAddr.sin_zero, 0x00, 8);
if(true)
{
memset(SendBuffer,0,sizeof(SendBuffer));
strcmp(SendBuffer,"hello");
iResult=sendto(ClientSocket,SendBuffer,sizeof(SendBuffer),0,(sockaddr *)&ServerAddr,len);
cout<<"Client OK!"<<endl;
SYSTEMTIME curr_st;
SYSTEMTIME time;
//recv(ClientSocket,(char *)&time,sizeof(time),0);
iResult = recvfrom(ClientSocket,(char *)&time,sizeof(time),0,(sockaddr *)&ServerAddr,&len);
if(!iResult)
{
cout<<"Receive Failed:"<<GetLastError()<<endl;
WSACleanup();
return -1;
}
curr_st.wYear=time.wYear-1;
curr_st.wMonth=time.wMonth;
curr_st.wDay=time.wDay;
curr_st.wHour=time.wHour;
curr_st.wMinute=time.wMinute;
curr_st.wSecond=time.wSecond;
curr_st.wDayOfWeek=time.wDayOfWeek;
curr_st.wMilliseconds=time.wMilliseconds;
cout<<"Receive Time:"<<curr_st.wYear<<"/"<<curr_st.wMonth<<"/"<<curr_st.wDay<<" the day of week is "<<curr_st.wDayOfWeek<<" "<<curr_st.wHour<<":"<<curr_st.wMinute<<"'"<<curr_st.wSecond<<"''"<<curr_st.wMilliseconds<<endl;
iResult = SetLocalTime(&curr_st);
if(!iResult)
{
cout<<"Failed to adjust SetTime privilege!"<<endl;
closesocket(ClientSocket);
WSACleanup();
return -1;
}
getchar();
getchar();
cout<<"success"<<endl;
}
closesocket(ClientSocket);
WSACleanup();
return 0;
}
io_tcpclient
#include <iostream>
#include <winsock2.h>
#include <fstream>
#include <string>
using namespace std;
#pragma comment(lib, "ws2_32.lib")
#define BUFFER_LEN 1024
int main(int argc, char * argv[])
{
WSADATA Ws;
SOCKET ClientSocket;
int iResult;
char IP_ADDRESS[15];
unsigned short int PORT;
if ( WSAStartup(MAKEWORD(2,2), &Ws) != 0 )
{
cout<<"Init Windows Socket Failed::"<<GetLastError()<<endl;
return -1;
}
//Create Socket
ClientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if ( ClientSocket == INVALID_SOCKET )
{
cout<<"Create Socket Failed::"<<GetLastError()<<endl;
return -1;
}
cout<<"Input Server IP:"<<endl;
cin>>IP_ADDRESS;
cout<<"Input Server PORT:"<<endl;
cin>>PORT;
struct sockaddr_in ServerAddr;
ServerAddr.sin_family = AF_INET;
ServerAddr.sin_addr.s_addr = inet_addr(IP_ADDRESS);
ServerAddr.sin_port = htons(PORT);
int len = sizeof(ServerAddr);
iResult = connect(ClientSocket,(struct sockaddr*)&ServerAddr, sizeof(ServerAddr));
if ( iResult == SOCKET_ERROR )
{
cout<<"Connect Error::"<<GetLastError()<<endl;
WSACleanup();
closesocket(ClientSocket);
return -1;
}
else
{
cout<<"Success"<<endl;
}
char SendBuffer[BUFFER_LEN];
if (true )
{
memset(SendBuffer,0,sizeof(SendBuffer));
strcmp(SendBuffer,"hello");
send(ClientSocket,SendBuffer,sizeof(SendBuffer),0);
SYSTEMTIME curr_st;
SYSTEMTIME time;
iResult = recv(ClientSocket, (char *)&time, sizeof(time), 0);
if(iResult == 0 || iResult == SOCKET_ERROR)
{
cout<<"Received Error!"<<endl;
closesocket(ClientSocket);
WSACleanup();
return -1;
}
curr_st.wYear=time.wYear-1;
curr_st.wMonth=time.wMonth;
curr_st.wDay=time.wDay;
curr_st.wHour=time.wHour;
curr_st.wMinute=time.wMinute;
curr_st.wSecond=time.wSecond;
curr_st.wDayOfWeek=time.wDayOfWeek;
curr_st.wMilliseconds=time.wMilliseconds;
cout<<"Receive Time:"<<curr_st.wYear<<"/"<<curr_st.wMonth<<"/"<<curr_st.wDay<<" the day of week is "<<curr_st.wDayOfWeek<<" "<<curr_st.wHour<<":"<<curr_st.wMinute<<"'"<<curr_st.wSecond<<"''"<<curr_st.wMilliseconds<<endl;
iResult = SetLocalTime(&curr_st);
if(!iResult)
{
int qwewq;
cin>>qwewq;
cout<<"Failed to adjust SetTime privilege"<<endl;
closesocket(ClientSocket);
WSACleanup();
return -1;
}
cout<<"success"<<endl;
}
closesocket(ClientSocket);
WSACleanup();
return 0;
}
截图
由于修改本地时间需要管理员权限,所以我在后面加了个判断,若无管理员权限,则输出
Failed to adjust SetTime privilege!
注:在修改时间时,必须完全获得所有的参数值,意思是
curr_st.wYear=XXXXX
curr_st.wMonth=XXXX
curr_st.wDay=XXX
curr_st.wHour=XXX
curr_st.wMinute=XXX
curr_st.wSecond=XXX
curr_st.wDayOfWeek=XXX
curr_st.wMilliseconds=XXX
才能修改成功
如果利用管理员权限的时候运行
直接运行exe会之间修改后闪退
暂时做了一个年份-1但其余时间不变的进行测试
测试的时候修改时间成功!!!
多路复用特点和应用:
除了可以采用多进程和多线程方法实现并发服务器之外,还可以采用I/O多路复用技术。通过该技术,系统内核缓冲I/O数据,当某个I/O准备好后,系统通知应用程序该I/O可读或可写,这样应用程序可以马上完成相应的I/O操作,而不需要等待系统完成相应I/O操作,从而应用程序不必因等待I/O操作而阻塞。
与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。
对于I/O复用典型的应用如下:
- 当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用。
- 当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。
- 如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。
- 如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。
- 如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。