/*
*客户端与服务端的实现代码分别封装在两个函数中,可以在主函数中通过选择分别执行客户端或服务端代码
*实现客户端与服务端简单字符串通信(字符串长度有限制)
*服务端只能够同时连接一个客户端并与其通信
*服务端能够获取所连接客户端的IP与端口号
*/
#include <Ws2tcpip.h>//htons()等函数
#include "winsock2.h"//bind()、listen()等函数
//第2版本的网络库
#pragma comment(lib, "ws2_32.lib")
#include <iostream>
using namespace std;
int TestServer();
int TestClient();
const int BUF_SIZE = 64; //接收/发送最大字节数
SOCKET sServer; //在服务端作为连接套结字,在客户端连接套结字与通信套结字一体
SOCKET sClient; //通信套结字(用于服务端,每当有一个客户端与服务端连接成功,服务端都会创建一个新的通信套结字,在本例子中只有一个通信套结字,所以服务端只能同时连接一个客户端并与一个客户端通信)
WSADATA wsd; //WSAStartup()函数会将环境的网络库信息存在这个结构体中。
SOCKADDR_IN servAddr; //服务器地址
char bufRecv[BUF_SIZE]; //接收数据缓冲区
char sendBuf[BUF_SIZE]; //发送数据缓冲区
int retVal; //返回值
int main()
{
//初始化套结字动态库
//WORD wdVersion = MAKEWORD(2, 2);//定义自己需要的网络库版本,这里是2.2
if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0) {//WSAStartup必须是应用程序或DLL调用的第一个Windows Sockets函数。它允许应用程序或DLL指明Windows Sockets API的版本号及获得特定Windows Sockets实现的细节。应用程序或DLL只能在一次成功的WSAStartup()调用之后才能调用进一步的Windows Sockets API函数。
cout << " WSAStartup failed " << endl;
return -1;
}
/*创建套接字
*int socket(int domain, int type, int protocal)
**domain 参数用来指定协议族,比如 AF_INET 用于 IPV4、AF_INET6 用于 IPV6、AF_LOCAL / AF_UNIX 用于本机;
**type 参数用来指定通信特性,比如 SOCK_STREAM 表示的是字节流,对应 TCP、SOCK_DGRAM 表示的是数据报,对应 UDP、SOCK_RAW 表示的是原始套接字
**protocal 参数原本是用来指定通信协议的,但现在基本废弃。因为协议已经通过前面两个参数指定完成,protocol 目前⼀般写成 0 即可;(在Windows下枚举类型IPPROTO_UDP= 17,IPPROTO_TCP= 6)
*/
sServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (INVALID_SOCKET == sServer){
cout << " socket failed " << endl;
WSACleanup();//释放套接字资源;
return -1;
}
int choice;
cout << "请选择【1-服务端,2-客户端】:";
cin >> choice;
switch (choice){
case 1:
TestServer();
break;
case 2:
TestClient();
break;
}
return 0;
}
//服务端
int TestServer()
{
//服务器套接字地址属性设置
servAddr.sin_family = AF_INET;//指定协议族:IPV4
servAddr.sin_port = htons(4999);//绑定端口号
servAddr.sin_addr.s_addr = INADDR_ANY;//INADDR_ANY就是指定地址为0.0.0.0的地址,这个地址事实上表示不确定地址或任意地址(以为可能服务器有多个网卡,即有多个IP,该设置表明服务端会侦听本机所有网卡/IP地址)
//绑定套接字
retVal = bind(sServer, (LPSOCKADDR)&servAddr, sizeof(SOCKADDR_IN));
if (SOCKET_ERROR == retVal){
cout << " Bind Fail" << endl;
closesocket(sServer); //关闭套接字
WSACleanup(); //释放套接字资源;
return -1;
}
//开始监听
//listen函数仅由TCP服务调用,它做两件事
//1.当socket函数创建一个套接字时,它被假设为一个主动套接字,也就是说,它是一个将调用connect连接的客户套接字。listen函数把一个未连接的套接字转换成一个被动套接字,指示内核应该接受指向该套接字的连接请求。调用listen导致套接字从CLOSED状态转换到LISTEN状态。
//listen函数的backlog参数曾被规定为未完成连接队列和已完成连接队列两个队列总和的最大值
retVal = listen(sServer, 1);
if (SOCKET_ERROR == retVal){
cout << " Listen Fail " << endl;
closesocket(sServer); //关闭套接字
WSACleanup(); //释放套接字资源;
return -1;
}
//接受客户端请求
sockaddr_in addrClient;
int addrClientlen = sizeof(addrClient);
sClient = accept(sServer, (SOCKADDR *)&addrClient, &addrClientlen);//在此服务端创建一个通信套结字
if (INVALID_SOCKET == sClient){//连接失败
cout << " Connect Fail " << endl;
closesocket(sServer); //关闭套接字
WSACleanup(); //释放套接字资源;
return -1;
}
else {//连接成功
//获取连接客户端的IP与端口号
/*
*int getsockname(int sockfd, struct sockaddr *localaddr, socklen_t *addrlen);
*int getpeername(int sockfd, struct sockaddr *peeraddr, socklen_t *addrlen);
*getsockname()函数:主要用于客户端,获取由内核赋予该连接的本地IP地址和本地端口号
*getpeername()函数:主要用于服务端,来获取当前连接的客户端的IP地址和端口号
*/
getpeername(sClient, (SOCKADDR *)&addrClient, &addrClientlen);
char ip[20]; //存放点分十进制IP地址
/*
*const char *inet_ntop(int af, const void *src, char *dst, socklen_t cnt);
*af:协议簇
*src:网络二进制IP地址
*dst:点分十进制IP地址
*cnt:IP地址长度
*返回值就是转换后的IP地址
*/
//ntohs()函数为端口号转换函数,二进制转十进制
cout << inet_ntop(AF_INET, &addrClient.sin_addr, ip,sizeof(ip))<<":"<< ntohs(addrClient.sin_port) <<" connect success! " << endl;
//cout << ip << endl;
}
while (true) {
//接收客户端数据
ZeroMemory(bufRecv, BUF_SIZE);//实质memset((Destination),0,(Length))
retVal = recv(sClient, bufRecv, BUF_SIZE, 0);
if (SOCKET_ERROR == retVal){
cout << " Receive Fail" << endl;
closesocket(sServer); //关闭套接字
closesocket(sClient); //关闭套接字
WSACleanup(); //释放套接字资源;
return -1;
}
if (bufRecv[0] == '0')//当接收到以字符'0'开头的字符串时,本次通话结束
break;
cout << "Server Receive:" << bufRecv << endl;
//向客户端发送数据
ZeroMemory(sendBuf, BUF_SIZE);
cout << "Server Send:";
cin >> sendBuf;
send(sClient, sendBuf, strlen(sendBuf), 0);
}
//退出
closesocket(sServer); //关闭连接套接字
closesocket(sClient); //关闭通信套接字
WSACleanup(); //释放套接字资源;
}
//客户端
int TestClient()
{
//设置服务器地址属性值
servAddr.sin_family = AF_INET;//指定协议族:IPV4
//servAddr.sin_addr.s_addr = inet_addr("127.0.0.1");//IP地址转换函数(旧版),该版转换函数对IP地址的格式要求相对比较宽松
/*int inet_pton(int af, const char *src, void *dst);
*第一个参数af是地址簇,第二个参数*src是来源地址,第三个参数* dst接收转换后的数据。
*/
inet_pton(AF_INET, "127.0.0.1", &servAddr.sin_addr.s_addr);//IP地址转换函数(新版),实现IP地址从点分十进制向二进制转换
servAddr.sin_port = htons((short)4999);//端口号转换函数,十进制转为二级制
//连接服务器
retVal = connect(sServer, (LPSOCKADDR)&servAddr, sizeof(servAddr));
if (SOCKET_ERROR == retVal){//连接失败
cout << " connect failed " << endl;
closesocket(sServer); //关闭套接字
WSACleanup(); //释放套接字资源
return -1;
}
else {//连接成功
//获取连接客户端的IP与端口号
/*
*int getsockname(int sockfd, struct sockaddr *localaddr, socklen_t *addrlen);
*int getpeername(int sockfd, struct sockaddr *peeraddr, socklen_t *addrlen);
*getsockname()函数:主要用于客户端,获取由内核赋予该连接的本地IP地址和本地端口号
*getpeername()函数:主要用于服务端,来获取当前连接的客户端的IP地址和端口号
*/
sockaddr_in addrClient;
int addrClientlen = sizeof(addrClient);
getsockname(sServer, (SOCKADDR *)&addrClient, &addrClientlen);
char ip[20]; //存放点分十进制IP地址
cout << inet_ntop(AF_INET, &addrClient.sin_addr, ip, sizeof(ip)) << ":" << ntohs(addrClient.sin_port) << " connect success! " << endl;
}
while (true) {
//向服务器发送数据
ZeroMemory(sendBuf, BUF_SIZE);
cout << "Client Send:";
cin >> sendBuf;
retVal = send(sServer, sendBuf, strlen(sendBuf), 0);
if (SOCKET_ERROR == retVal){
cout << " send failed " << endl;
closesocket(sServer); //关闭套接字
WSACleanup(); //释放套接字资源
return -1;
}
if (sendBuf[0] == '0')
break;
ZeroMemory(bufRecv, BUF_SIZE);
recv(sServer, bufRecv, BUF_SIZE, 0); // 接收服务器端的数据, 只接收 BUF_SIZE 个字符
cout << "Client Receive:" << bufRecv << endl;
}
//退出
closesocket(sServer); //关闭连接与通信套接字(更确切地说,释放套接口描述字)
WSACleanup(); //释放套接字资源
return 0;
}