模拟实现TCP协议通信过程,要求编程实现服务器端与客户端之间双向数据传递。 客户端向服务器端发送“我是集美大学网络工程专业学生”,服务器回应“我也是集美大学网络工程专业学生”。
服务器端创建监听套接字,并为它关联一个本地地址(指定IP地址和端口号),然后进入监听状态准备接受客户的连接请求。为了接受客户端的连接请求,服务器端必须调用accept函数。
客户端创建套接字后即可调用connect函数去试图连接服务器监听套接字。当服务器端的accept函数返回后,connect函数也返回。此时客户端使用socket函数创建的套接字,服务器端使用accept函数创建的套接字,双方实现通信。
考虑到版本的变迁问题,担心热爱学习的友友们找不到VS2019版本的下载地址,这里特地给出:
VS2019
其中最重要的环境有以下的这几步:
initsock.h
的头文件的形式保存。// initsock.h文件
#include
#pragma comment(lib, "WS2_32") // 链接到 WS2_32.lib
class CInitSock
{
public:
/*CInitSock 的构造器*/
CInitSock(BYTE minorVer = 2, BYTE majorVer = 2)
{
// 初始化WS2_32.dll
WSADATA wsaData;
WORD sockVersion = MAKEWORD(minorVer, majorVer);
if (::WSAStartup(sockVersion, &wsaData) != 0)
{
exit(0);
}
}
/*CInitSock 的析构器*/
~CInitSock()
{
::WSACleanup();
}
};
#pragma comment(lib, "WS2_32") // 链接到 WS2_32.lib
WSAStartup()
函数,该函数用于加载Winsock库,该函数需要传入两个参数,一个参数是WORD wVersionRequested
这个参数用于指定想要加载的Winsock库的版本号,该结构体有两个元素,一个是minorVer
次版本号,另一个是majorVer
主版本号,在创建这样的结构体的时候需要用MAKEWORD
函数来创建这样的一个结构体。WSAStartup
的调用都必须对应一个对WSACleanup
的调用,所以我们需要在析构器中调用这个函数来实现对Winsock库的释放。#include "initsock.h"
#include
using namespace std;
CInitSock initSock; // 初始化Winsock库
int main()
{
// 创建套接字
SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sListen == INVALID_SOCKET)
{
cout << "Failed socket()" << endl;
return 0;
}
// 填充sockaddr_in结构
sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(4567);
sin.sin_addr.S_un.S_addr = INADDR_ANY;
// 绑定这个套接字到一个本地地址
if (::bind(sListen, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR)
{
cout << "Failed bind()" << endl;
return 0;
}
// 进入监听模式
if (::listen(sListen, 2) == SOCKET_ERROR)
{
cout << "Failed listen()" << endl;
return 0;
}
// 循环接受客户的连接请求
sockaddr_in remoteAddr;
int nAddrLen = sizeof(remoteAddr);
SOCKET sClient;
char szText[] = "你好,客户端:我也是集美大学网络工程专业学生!";
while (TRUE)
{
cout << "服务端已启动,正在监听!\n" << endl;
// 接受一个新连接
sClient = ::accept(sListen, (SOCKADDR*)&remoteAddr, &nAddrLen);
if (sClient == INVALID_SOCKET)
{
cout << "Failed accept()" << endl;
continue;
}
cout << "与主机 " << inet_ntoa(remoteAddr.sin_addr) << "建立连接:" << endl;
// 接收数据
char buff[256];
int nRecv = ::recv(sClient, buff, 256, 0);
if (nRecv > 0)
{
buff[nRecv] = '\0';
cout << "接收到数据:" << buff << endl;
}
// 向客户端发送数据
::send(sClient, szText, strlen(szText), 0);
// 关闭同客户端的连接
::closesocket(sClient);
}
// 关闭监听套接字
::closesocket(sListen);
return 0;
}
#include "initsock.h"
#include
using namespace std;
CInitSock initSock; // 初始化Winsock库
int main()
{
// 创建套接字
SOCKET s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (s == INVALID_SOCKET)
{
cout << " Failed socket()" << endl;
return 0;
}
// 也可以在这里调用bind函数绑定一个本地地址,否则系统将会自动安排
// 填写远程地址信息
sockaddr_in servAddr;
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(4567);
// 填写服务器程序(TCPServer程序)所在机器的IP地址
char serverAddr[] = "127.0.0.1";
servAddr.sin_addr.S_un.S_addr = inet_addr(serverAddr);
//与服务器建立连接
if (::connect(s, (sockaddr*)&servAddr, sizeof(servAddr)) == -1)
{
cout << " Failed connect()" << endl;
return 0;
}
cout << "与服务器 " << serverAddr << "建立连接" << endl;
//向服务器发送数据
char szText[] = "你好,服务器:我是集美大学网络工程专业学生!";
int slen = send(s, szText, 100, 0);
if (slen > 0)
{
cout << "向服务器发送数据:" << szText << endl;
}
// 接收数据
char buff[256];
int nRecv = ::recv(s, buff, 256, 0);
if (nRecv > 0)
{
buff[nRecv] = '\0';
cout << "接收到数据:" << buff << endl;
}
// 关闭套接字
::closesocket(s);
return 0;
}
socket()
函数):SOCKET socket(
int af, //用来指定套接字使用的地址格式,WinSock中只支持AF_INET
int type, //用来指定套接字的类型
int protocol //配合type参数使用,用来指定使用的协议类型。可以是IPPPROTO_TCP等
);
除了socket()
函数之外,Winsock2还提供了WSASocket()
函数来创建套接字,与socket()
相比,它提供了更多的参数。
int closesocket(SOCKET s); //函数唯一的参数就是要关闭的套接字的句柄
struct sockaddr
{
u_short sa_family;
char sa_data[14];
};
struct sockaddr_in{
short sin_family; //地址家族(即指定地址格式),应为AF_INET
u_short sin_port; //端口号
struct in_addr sin_addr; //IP地址
char sin_zero[8]; //空字节,要设为0,主要是为了和struct sockaddr的长度一致
};
struct in_addr {
union{
struct {u_char s_b1,s_b2,s_b3,s_b4;} S_un_b; //以4个u_char来描述
struct {u_short s_w1,s_w2;} S_un_w; //以2个u_short来描述
u_long S_addr; //以1个u_long来描述
} S_un;
};
unsigned long inet_addr(const char* cp); //将一个“aa.bb.cc.dd”(点分十进制)类型的IP地址字符串转化为32位的二进制数。
char* inet_ntoa(struct in_addr in); //将32位的二进制数转化为IP地址字符串。
inet_addr()
返回的32位二进制数是用网络顺序存储【也成为大尾方式】的。0x12345678
为例:0x12
、0x34
、0x56
、0x78
的顺序来存。0x78
、0x56
、0x34
、0x12
的顺序来存。u_short htons(u_short hostshort) //将u_short类型的变量从主机字节顺序转化到TCP/IP的网络字节顺序
u_long htonl(u_long hostlong) //将u_long类型的变量从主机字节顺序转化到TCP/IP的网络字节顺序
u_short ntohs(u_short netshort) //将u_short类型的变量从转化TCP/IP的网络字节顺序到主机字节顺序
u_long ntohl(u_long netlong) //将u_long类型的变量从转化TCP/IP的网络字节顺序到主机字节顺序
sockaddr_in
的填充中使用: // 填充sockaddr_in结构
sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(4567);
sin.sin_addr.S_un.S_addr = INADDR_ANY;
INADDR_ANY
,指定端口号为0
.INADDR_ANY
,系统会自动使用当前主机配置的所有IP地址,简化程序设计。0
,程序执行时系统会为这个应用程序分配唯一的端口号,其值在1024-5000之间。应用程序可以在
bind
之后,使用getsockname
来知道它分配的地址。但是得注意,得等连接上之后才能看到。
// 填写远程地址信息
sockaddr_in servAddr;
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(4567);
// 填写服务器程序(TCPServer程序)所在机器的IP地址
char serverAddr[] = "127.0.0.1";
servAddr.sin_addr.S_un.S_addr = inet_addr(serverAddr);
127.0.0.1
是本地的回环地址,用于本地测试所使用的地址,可以视为是在本地的服务器所在的IP地址,本地的客户端可以通过绑定该IP地址来实现与本地的服务器之间的通信。
int bind(
SOCKET s, //套接字的句柄
const struct sockaddr* name, //要关联的本地地址
int namelen //地址的长度,一般涉及sizeof函数
);
- 往往我们填写的地址的用sockaddr_in的结构来填充的,所以往往涉及一个类型转换 (LPSOCKADDR)
// 绑定这个套接字到一个本地地址
if (::bind(sListen, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR)
{
cout << "Failed bind()" << endl;
return 0;
}
int listen(
SOCKET s, //套接字的句柄
int backlog //监听队列中允许保持的尚未处理的最大连接数量
);
listen函数为到达的连接指定backlog
listen函数仅应用在支持连接的套接字上,如SOCKET_STREAM。
// 进入监听模式
if (::listen(sListen, 2) == SOCKET_ERROR)
{
cout << "Failed listen()" << endl;
return 0;
}
SOCKET accept(
SOCKET s, //套接字句柄
struct sockaddr* addr, //一个指向sockaddr_in结构的指针,包含了要连接的服务器的地址信息
int* addrlen //一个指向地址长度的指针
);
该函数会在s中取出未处理连接中的第一个连接,然后为这个连接创建新的套接字,返回它的句柄。新创建的套接字是专门用来处理实际连接中的通信的套接字。
// 循环接受客户的连接请求
sockaddr_in remoteAddr;
int nAddrLen = sizeof(remoteAddr);
SOCKET sClient;
char szText[] = "你好,客户端:我也是集美大学网络工程专业学生!";
while (TRUE)
{
cout << "服务端已启动,正在监听!\n" << endl;
// 接受一个新连接
sClient = ::accept(sListen, (SOCKADDR*)&remoteAddr, &nAddrLen);
if (sClient == INVALID_SOCKET)
{
cout << "Failed accept()" << endl;
continue;
}
cout << "与主机 " << inet_ntoa(remoteAddr.sin_addr) << "建立连接:" << endl;
// 接收数据
char buff[256];
int nRecv = ::recv(sClient, buff, 256, 0);
if (nRecv > 0)
{
buff[nRecv] = '\0';
cout << "接收到数据:" << buff << endl;
}
// 向客户端发送数据
::send(sClient, szText, strlen(szText), 0);
// 关闭同客户端的连接
::closesocket(sClient);
}
int connect(
SOCKET s, //套接字句柄
const struct sockaddr FAR* name, //一个指向sockaddr_in结构的指针,包含了要连接的服务器的地址信息
int namelen //sockaddr_in结构的长度
);
s是客户端的套接字,name和namelen用于寻址正在监听的处在服务器端的套接字。
// 填写远程地址信息
sockaddr_in servAddr;
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(4567);
// 填写服务器程序(TCPServer程序)所在机器的IP地址
char serverAddr[] = "127.0.0.1";
servAddr.sin_addr.S_un.S_addr = inet_addr(serverAddr);
//与服务器建立连接
if (::connect(s, (sockaddr*)&servAddr, sizeof(servAddr)) == -1)
{
cout << " Failed connect()" << endl;
return 0;
}
int send(
SOCKET s, //套接字的句柄
const cahr FAR* buf, //要发送数据的缓冲区的地址
int len, //缓冲区的长度
int flags //指定了调用方式,通常设为0
);
int recv(
SOCKET s, //套接字的句柄
cahr FAR* buf, //要接收的数据的缓冲区的地址
int len, //缓冲区的长度
int flags //指定了调用方式,通常设为0
);