实验目的:
学习SOCKET编程基础
实验要求:
- 程序使用非阻塞方式进行通信;
- 使得服务器可以同时和两个客户机相互发送信息,客户端所显示的IP地址和端口是客户自己的。服务器端所显示的IP地址和端口也是客户的。(效果如下图)
实验设备:vs2013 (如果你是使用6.0版本的,详情请看我的博客“TCPIP实验1、2”中的开发提示。如果开发过程中有什么问题,也建议将我这部分的专栏都浏览一遍,或许能找到我也同样遇到过的问题的解决方案。)
基于非阻塞模式的多线程服务器应用程序的编程,需要打开至少一个作为服务端的vs2013工程,至少两个作为客户端 的vs2013工程。
实验整体运行效果:
流程图:
服务器端代码:
// 大作业服务器端.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <WINSOCK2.H>
#include <iostream>
#include <string>
#pragma comment(lib,"WS2_32.lib")
#define BUF_SIZE 64 // 缓冲区大小
sockaddr_in addrClient; // 客户端地址
DWORD WINAPI AnswerThread(LPVOID lparam)
{
char buf[BUF_SIZE]; // 用于接受客户端数据的缓冲区
int retVal; // 调用各种Socket函数的返回值
SOCKET sClient = (SOCKET)(LPVOID)lparam;
// 循环接收客户端的数据,直接客户端发送quit命令后退出。
while (true)
{
ZeroMemory(buf, BUF_SIZE); // 清空接收数据的缓冲区
retVal = recv(sClient, buf, BUFSIZ, 0); // 接收来自客户端的数据,因为是非阻塞模式,所以即使没有数据也会继续
if (SOCKET_ERROR == retVal)
{
int err = WSAGetLastError(); // 获取错误编码
if (err == WSAEWOULDBLOCK) // 接收数据缓冲区暂无数据
{
Sleep(100);
continue;
}
else if (err == WSAETIMEDOUT || err == WSAENETDOWN)
{
printf("recv failed !\n");
closesocket(sClient);
WSACleanup();
return -1;
}
}
if (retVal == 0){
printf("用户以下线");
}
// 获取当前系统时间
SYSTEMTIME st;
GetLocalTime(&st);
char sDateTime[30];
sprintf_s(sDateTime, "%4d-%2d-%2d %2d:%2d:%2d", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
// 打印输出的信息
printf("时间:%s,客户端IP和端口号: [%s:%d] 客户端名称:%s\n", sDateTime, inet_ntoa(addrClient.sin_addr), addrClient.sin_port, buf);
// 如果客户端发送quit字符串,则服务器退出
if (strcmp(buf, "quit") == 0)
{
retVal = send(sClient, "quit", strlen("quit"), 0);
break;
}
else // 否则向客户端发送回显字符串
{
char msg[BUF_SIZE];
sprintf_s(msg, " %s", buf);
//___________________________________________________
// 向服务器发送数据
printf("Please input a string to send: ");
// 接收输入的数据
std::string str;
std::getline(std::cin, str);
// 将用户输入的数据复制到buf中
ZeroMemory(buf, BUF_SIZE);
strcpy_s(buf, str.c_str());
//---------------------------------------------------
while (true)
{
// 向服务器发送数据
retVal = send(sClient, buf, strlen(buf), 0);
if (SOCKET_ERROR == retVal)
{
int err = WSAGetLastError();
if (err == WSAEWOULDBLOCK) // 无法立即完成非阻塞套接字上的操作
{
Sleep(500);
continue;
}
else
{
printf("send failed !\n");
closesocket(sClient);
WSACleanup();
return -1;
}
}
break;
}
}
}
// 关闭套接字
closesocket(sClient);
}
int _tmain(int argc, _TCHAR* argv[])
{
WSADATA wsd; // WSADATA变量,用于初始化Windows Socket
SOCKET sServer; // 服务器套接字,用于监听客户端请求
SOCKET sClient; // 客户端套接字,用于实现与客户端的通信
int retVal; // 调用各种Socket函数的返回值
// 初始化套接字动态库
if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0)
{
printf("WSAStartup failed !\n");
return 1;
}
// 创建用于监听的套接字
sServer = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
if (INVALID_SOCKET == sServer)
{
printf("socket failed !\n");
WSACleanup();
return -1;
}
// 设置套接字为非阻塞模式
int iMode = 1;
retVal = ioctlsocket(sServer, FIONBIO, (u_long FAR*) &iMode);
if (retVal == SOCKET_ERROR)
{
printf("ioctlsocket failed !\n");
WSACleanup();
return -1;
}
// 设置服务器套接字地址
SOCKADDR_IN addrServ;
addrServ.sin_family = AF_INET;
addrServ.sin_port = htons(9990); // 监听端口为9990
addrServ.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
//addrServ.sin_addr.S_un.S_addr = inet_addr("192.168.43.62");
// 绑定套接字sServer到本地地址,端口9990
retVal = bind(sServer, (const struct sockaddr*)&addrServ, sizeof(SOCKADDR_IN));
if (SOCKET_ERROR == retVal)
{
printf("bind failed !\n");
closesocket(sServer);
WSACleanup();
return -1;
}
// 监听套接字
retVal = listen(sServer, SOMAXCONN);
if (SOCKET_ERROR == retVal)
{
printf("listen failed !\n");
closesocket(sServer);
WSACleanup();
return -1;
}
// 接受客户请求
printf("服务端启动,可以接受多个客户端同时连接...\n");
int addrClientlen = sizeof(addrClient);
// 循环等待
while (true)
{
sClient = accept(sServer, (sockaddr FAR*)&addrClient, &addrClientlen);
if (INVALID_SOCKET == sClient)
{
int err = WSAGetLastError();
if (err == WSAEWOULDBLOCK) // 无法立即完成非阻塞套接字上的操作
{
Sleep(100);
continue;
}
//else
//{
// //printf("accept failed !\n");
// //closesocket(sServer);
// //WSACleanup();
// //return -1;
// Sleep(100);
// continue;
//}
}
// 创建专用通信线程
DWORD dwThreadId;
CreateThread(NULL, NULL, AnswerThread, (LPVOID)sClient, 0, &dwThreadId);
}
// 释放套接字
closesocket(sServer);
WSACleanup();
// 暂停,按任意键退出
system("pause");
return 0;
}
客户端代码:
// 大作业-客户端2.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <Winsock2.H>
#include <string>
#include <iostream>
#pragma comment(lib,"WS2_32.lib")
#define BUF_SIZE 64 // 缓冲区大小
int _tmain(int argc, _TCHAR* argv[])
{
WSADATA wsd; // 用于初始化Windows Socket
SOCKET sHost; // 与服务器进行通信的套接字
SOCKADDR_IN servAddr; // 服务器地址
char buf[BUF_SIZE]; // 用于接受数据缓冲区
int retVal; // 调用各种Socket函数的返回值
// 初始化Windows Socket
if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0)
{
printf("WSAStartup failed !\n");
return 1;
}
// 创建套接字
sHost = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
if (INVALID_SOCKET == sHost)
{
printf("socket failed !\n");
WSACleanup();
return -1;
}
// 设置套接字为非阻塞模式
int iMode = 1;
retVal = ioctlsocket(sHost, FIONBIO, (u_long FAR*) &iMode);
if (retVal == SOCKET_ERROR)
{
printf("ioctlsocket failed !\n");
WSACleanup();
return -1;
}
// 设置服务器地址
servAddr.sin_family = AF_INET;
//servAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY); // 用户需要根据实际情况修改
servAddr.sin_addr.S_un.S_addr = inet_addr("192.168.43.62");
servAddr.sin_port = htons(9990); // 在实际应用中,建议将服务器的IP地址和端口号保存在配置文件中
int sServerAddlen = sizeof(servAddr); // 计算地址的长度
// 循环等待
while (true)
{
// 连接服务器
Sleep(200);
retVal = connect(sHost, (LPSOCKADDR)&servAddr, sizeof(servAddr));
Sleep(200);
if (SOCKET_ERROR == retVal)
{
int err = WSAGetLastError();
if (err == WSAEWOULDBLOCK || err == WSAEINVAL) // 无法立即完成非阻塞套接字上的操作
{
//Sleep(500);
continue;
}
else if (err == WSAEISCONN) // 已建立连接
{
break;
}
else
{
continue;
//printf("connect failed !\n");
//closesocket(sHost);
//WSACleanup();
//return -1;
}
}
}
// 循环向服务器发送字符串,并显示反馈信息。
// 发送quit将使服务器程序退出,同时客户端程序自身也将退出
while (true)
{
// 向服务器发送数据
printf("Please input a string to send: ");
// 接收输入的数据
std::string str;
std::getline(std::cin, str);
// 将用户输入的数据复制到buf中
ZeroMemory(buf, BUF_SIZE);
strcpy_s(buf, str.c_str());
// 循环等待
while (true)
{
// 向服务器发送数据
retVal = send(sHost, buf, strlen(buf), 0);
if (SOCKET_ERROR == retVal)
{
int err = WSAGetLastError();
if (err == WSAEWOULDBLOCK) // 无法立即完成非阻塞套接字上的操作
{
Sleep(500);
continue;
}
else
{
printf("send failed !\n");
closesocket(sHost);
WSACleanup();
return -1;
}
}
break;
}
while (true)
{
ZeroMemory(buf, BUF_SIZE); // 清空接收数据的缓冲区
retVal = recv(sHost, buf, sizeof(buf)+1, 0); // 接收服务器回传的数据
if (SOCKET_ERROR == retVal)
{
int err = WSAGetLastError(); // 获取错误编码
if (err == WSAEWOULDBLOCK) // 接收数据缓冲区暂无数据
{
Sleep(100);
//printf("waiting back msg !\n");
continue;
}
else if (err == WSAETIMEDOUT || err == WSAENETDOWN)
{
printf("recv failed !\n");
closesocket(sHost);
WSACleanup();
return -1;
}
break;
}
break;
}
//ZeroMemory(buf,BUF_SIZE); // 清空接收数据的缓冲区
//retVal = recv(sHost,buf,sizeof(buf)+1,0); // 接收服务器回传的数据
// 获取当前系统时间
SYSTEMTIME st;
GetLocalTime(&st);
char sDateTime[30];
sprintf_s(sDateTime, "%4d-%2d-%2d %2d:%2d:%2d", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
// 打印输出的信息
printf("时间:%s,服务端IP和端口号: [%s:%d] 服务端名称:%s\n", sDateTime, inet_ntoa(servAddr.sin_addr), servAddr.sin_port, buf);
// 如果收到quit,则退出
if (strcmp(buf, "quit") == 0)
{
printf("quit!\n");
break;
}
}
// 释放资源
closesocket(sHost);
WSACleanup();
// 暂停,按任意键继续
system("pause");
return 0;
}
上面的实验存在一个问题,实验默认客户端是不会掉线的,但是当客户端掉线的时候,服务端会输出很多没必要的提示信息,我也在找解决办法,稍安勿躁