🔥博客主页: 我要成为C++领域大神
🎥系列专栏:【C++核心编程】 【计算机网络】 【Linux编程】 【操作系统】
❤️感谢大家点赞👍收藏⭐评论✍️
本博客致力于分享知识,欢迎大家共同学习和交流。
TCP客户端搭建过程
1、加载库
2、创建socket套接字
3、连接
4、收发数据
5、关闭套接字
6、卸载库
TCP服务器搭建流程
服务器的特点:端口和IP地址必须绑定
1、加载库
2、创建套接字
3、绑定IP
4、监听
5、接受连接
6、接收客户端发送的数据
7、返回客户端一些信息
8、关闭套接字
9、卸载库
服务端
TCP通信的服务端在UDP通信服务端的基础上,增加了监听和接受连接的步骤。另外接收数据和发送数据使用的是recv函数和send函数。
加载库
#include<winsock2.h>
#pragma comment(lib,"ws2_32.lib")
#pragma comment(lib,"Ws2_32.lib")表示链接Ws2_32.lib这个库。
//1、加载库
WORD version = MAKEWORD(2, 2);
WSADATA data = {};
int err=WSAStartup(version,&data);
if (0 != err) {
cout << "WSAStartup fail" << endl;
return 1;
}
else {
cout << "WSAStartup success" << endl;
}
if (2 != HIBYTE(data.wVersion) || 2 != LOBYTE(data.wVersion)) {
//虽然加载库成功了,但是版本加载不对
cout << "version error" << endl;
WSACleanup();
return 1;
}
else {
cout << "WSAStartup version success" << endl;
}
创建套接字
创建套接字需要使用socket函数。函数的3个参数,分别代表 地址族,套接字类型,通讯协议
根据返回值是否为异常,来判断套接字创建是否成功。
若创建失败,需要卸载库,并终止运行。
SOCKET WSAAPI socket(
//VS官方帮助文档中的[in]表示输入的参数
[in] int af,
[in] int type,
[in] int protocol
);
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (INVALID_SOCKET == sock) {
cout << "socket fail" << endl;
WSACleanup();
return 1;
}
else
{
cout << "socket success" << endl;
}
绑定IP和端口号
绑定IP地址使用的函数是bind函数。函数的返回值是一个整型,根据函数的返回值来判断IP绑定是否成功,失败返回SOCKET_ERROR,此时不仅要卸载库,还要关闭套接字。
bind函数有三个参数,套接字,sockaddr结构体类型的指针,结构体大小
int WSAAPI bind(
[in] SOCKET s,
[in] const sockaddr *name,
[in] int namelen
);
通过帮助文档查看sockaddr结构体,给出了sockaddr和sockaddr_in。为了方便设置端口号和IP,我们使用sockaddr_in,传入函数时,再强制类型转换将其地址转换为sockaddr*类型。
typedef struct sockaddr {
u_short sa_family;
char sa_data[14];
} SOCKADDR, *PSOCKADDR, *LPSOCKADDR;
typedef struct sockaddr_in {
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
} SOCKADDR_IN, *PSOCKADDR_IN, *LPSOCKADDR_IN;
sockaddr_in结构体的成员有一个in_addr结构体,在帮助文档中搜索查看,发现结构体里面是个联合体。用于传入IP。
struct in_addr {
union {
struct {
u_char s_b1;
u_char s_b2;
u_char s_b3;
u_char s_b4;
} S_un_b;
struct {
u_short s_w1;
u_short s_w2;
} S_un_w;
u_long S_addr;
} S_un;
};
//3、绑定IP地址
sockaddr_in addServer = {};
addServer.sin_family = AF_INET;
addServer.sin_port = htons(98765);
addServer.sin_addr.S_un.S_addr= INADDR_ANY;
err = bind(sock, (sockaddr*)&addServer, sizeof(addServer));
if (err == SOCKET_ERROR) {
cout << "bind fail:" << WSAGetLastError() << endl;
closesocket(sock);
WSACleanup();
return 1;
}
else
{
cout << "bind success" << endl;
}
这里需要注意的是,我们在设置端口号时,需要转一下网络字节序。如果不转换的话,PC之间的客户端和服务端进行数据传输,由于都是小端存储,所以不会有影响。但是如果对端不是PC,可能就会有问题。
sockServer.sin_port = htons(9876); //设置端口号,并转换网络字节序
监听
监听使用listen函数,监听socket,若正常监听返回0,否则返回SOCKET_ERROR。函数有两个参数,第一个参数是套接字,即监听所用套接字;第二个参数是处理连接队列的最大长度,监听是有监听队列的,即最多可以同时监听多少个连接。
err = listen(sock, 100);
if (err == SOCKET_ERROR) {
cout << "listen fail:" << WSAGetLastError() << endl;
closesocket(sock);
WSACleanup();
return 1;
}
else
{
cout << "listen success" << endl;
}
接受连接
TCP是面向连接的协议,所以需要去等待连接。
连接使用accept函数,默认也是阻塞状态。
函数的返回值是一个SOCKET类型的值,即实际连接的套接字的句柄。连接成功则返回连接产生的套接字,失败返回INVALID_SOCKET。第一个参数是输入参数,处于监听状态的套接字;第二个参数是输出参数,输出我们连接端的地址;第三个参数是输入输出参数,即第二个参数的长度。
SOCKET WSAAPI accept(
[in] SOCKET s,
[out] sockaddr *addr,
[in, out] int *addrlen
);
SOCKET sockTalk=accept(sock, (sockaddr*)&addr, &size);
if (INVALID_SOCKET != sock) {
cout << "accept fail" <<WSAGetLastError()<< endl;
return 1;
}
else
{
cout << "client ip:"<<inet_ntoa(addr.sin_addr)<<endl;
}
接收消息
与UDP不同的是,TCP接收消息使用的是recv函数。函数有4个参数,第一个参数是连接产生的套接字;第二个参数是输出参数,字符串类型,用来存放收到的消息;第三个参数是字符串长度;第四个参数是标志位(填0即可);
若能正常接收,则返回值>0;如果连接关闭,则返回0,如果接收异常返回错误信息。
recv参数中的socket就是连接成功产生的套接字,只对应一个IP,所以函数中不需要客户端IP参数了。
int WSAAPI recv(
[in] SOCKET s,
[out] char *buf,
[in] int len,
[in] int flags,
);
nRecvNum=recv(sockTalk, recvBuf, sizeof(recvBuf),1);
if (nRecvNum > 0) {
cout << "client say:" << recvBuf << endl;
}
else
{
cout << "recv error" << WSAGetLastError() << endl;
}
发送消息
同样的,发送消息使用的是send函数,函数有4个参数。第一个参数是连接产生的套接字,第二个参数是字符串,用来存放我们发送的内容,第三个参数是字符串的长度,第四个参数是标志位,填0即可。
int WSAAPI send(
[in] SOCKET s,
[in] const char *buf,
[in] int len,
[in] int flags,
);
nSendNum = send(sockTalk, sendBuf, strlen(sendBuf), 1);
if (nSendNum > 0) {
cout << "server say:" << sendBuf << endl;
}
else
{
cout << "send error" << WSAGetLastError() << endl;
}
}
关闭套接字
关闭我们上面创建的套接字,使用的是closesocket函数,传入的参数是套接字
//6、关闭套接字
closesocket(sock);
卸载库
使用WSACleanup函数即可,没有参数。
//7、卸载库
WSACleanup();
服务端代码
#include<iostream>
#include<Winsock2.h>
#pragma comment(lib,"Ws2_32.lib")
using namespace std;
int main() {
//1、加载库
WORD version=MAKEWORD(2,2);
WSADATA Data = {};
int err=WSAStartup(version, &Data);
if (err != 0) {
cout << "WSAStartup fail" << endl;
return 1;
}
if (2 != HIBYTE(Data.wVersion) || 2 != LOBYTE(Data.wVersion)) {
cout << "version err" << endl;
WSACleanup();
return 1;
}
else { cout << "WSAStartup success" << endl; }
//2、创建套接字
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (INVALID_SOCKET == sock) {
cout << "socket fail" << endl;
WSACleanup();
return 1;
}
else
{
cout << "socket success" << endl;
}
//3、绑定IP地址
sockaddr_in addServer = {};
addServer.sin_family = AF_INET;
addServer.sin_port = htons(98765);
addServer.sin_addr.S_un.S_addr= INADDR_ANY;
err = bind(sock, (sockaddr*)&addServer, sizeof(addServer));
if (err == SOCKET_ERROR) {
cout << "bind fail:" << WSAGetLastError() << endl;
closesocket(sock);
WSACleanup();
return 1;
}
else
{
cout << "bind success" << endl;
}
//4、监听
err = listen(sock, 100);
if (err == SOCKET_ERROR) {
cout << "listen fail:" << WSAGetLastError() << endl;
closesocket(sock);
WSACleanup();
return 1;
}
else
{
cout << "listen success" << endl;
}
sockaddr_in addr;
int size = sizeof(addr);
char recvBuf[4096] = "";
char sendBuf[4096] = "";
int nRecvNum = 0;
int nSendNum = 0;
while (true)
{//5、接受连接(产生的套接字专门用来跟连接的客户端进行通信)
SOCKET sockTalk=accept(sock, (sockaddr*)&addr, &size);
if (INVALID_SOCKET == sock) {
cout << "accept fail: " <<WSAGetLastError()<< endl;
return 1;
}
else
{
cout << "client ip:"<<inet_ntoa(addr.sin_addr)<<endl;
}
while (true) {
//6、接收数据
nRecvNum=recv(sockTalk, recvBuf, sizeof(recvBuf),0);
if (nRecvNum > 0) {
cout << "client say:" << recvBuf << endl;
}
else
{
cout << "recv error" << WSAGetLastError() << endl;
break;
}
//7、发送数据
gets_s(sendBuf);
nSendNum = send(sockTalk, sendBuf, strlen(sendBuf), 0);
if (nSendNum ==SOCKET_ERROR) {
cout << "send error" << WSAGetLastError() << endl;
break;
}
}
closesocket(sockTalk);//关闭专门用来通信的套接字
}
//8、关闭套接字,卸载库
closesocket(sock);
WSACleanup();
return 0;
}
客户端
与服务端相比,客户端没有绑定IP,监听和接受连接过程。但是需要与服务端进行连接。
连接
连接使用的函数是connect函数,connect函数的返回值是一个整型,若返回SOCKET_ERROR则说明连接失败,否则成功。函数有三个参数。第一个参数是创建的套接字;第二个参数是服务端的IP,第三个参数是第二个参数的大小。
int WSAAPI connect(
[in] SOCKET s,
[in] const sockaddr *name,
[in] int namelen
);
//连接服务端
sockaddr_in Server_addr;
Server_addr.sin_family = AF_INET;
Server_addr.sin_port = htons(98765);
Server_addr.sin_addr.S_un.S_addr = inet_addr("10.51.225.72");
err=connect(sock,(sockaddr*) & Server_addr, sizeof(Server_addr));
if (err == SOCKET_ERROR) {
cout << "connect fail" << endl;
cout << "ERROR:" << WSAGetLastError() << endl;
closesocket(sock);
WSACleanup();
return 1;
}
else
{
cout << "connect success" << endl;
}
运行结果