1 准备知识
1.1 环境
Windows环境下使用的VS2019.
1.2 需要的库
#include <iostream>
#include <string>
#include <WinSock2.h>
#pragma comment(lib,"ws2_32.lib")//加载ws2_32.ddl
1.3 sockaddr_in结构说明
可以配合底下全部源码看,更容易懂。
struct sockaddr_in{
short sin_family; //网络中标识不同设备使用的地址类型,对于IP类型为AF_INET
unsigned short sin_port;//Socket对应的端口号
IN_ADDR sin_addr; //一个对IP进行封装的结构
char sin_zer0[8]; //用来填充结构的数组,字符全为0,让sockaddr与sockaddr_in两个数据结构保持大小相同
};
1.4 WSAStartup()函数
使用WSAStartup()函数进行初始化,函数原型如下:
int WSAStartup(WORD sockVersion,LPWSADATA lpWSAData);
说明:sockVersion表示WinSock版本,应该用的就是2.2版本,lpWSAData是一个WSADATA结构的指针,记录的Windows套接字的相关信息。WSAStartup()调用成功返回0;
1.5 WSADATA
WSADATA wsa;
说明:用来存储被WSAStartup()函数调用后的套接字数据。
我去,才写这么点,昨天没睡好,困了我的妈呀,啊呀~~呀呀呀
1.6 socket函数
SOCKET socket(int af,int type,int procotal);
说明:
af:地址描述,目前只提供AF_INET地址格式;
type:套接字类型,分为三种:
SOCK_STREAM:创建面向连接的流式套接字(运用TCP/IP协议);
SOCK_DGRAM:创建无连接的数据报套接字(运用UDP协议);
SOCK_RAM:创建原始套接字。
protocal:通信协议,如果用户不指定,可以设为0。
返回值:如果成功返回socket对象,如果不成功返回INVALID_SOCKET。
1.7 bind函数
int bind(SOCKET s,const struct sockaddr FAR* name,int namelen);
说明:
s:一个套接字对象;
name:一个sockaddr结构指针。该地址中包含了要结合的地址和端口号,如果用户不在意地址和端口的值,可以设置为INADDR_ANY,即端口号为0,它可以被自动设为合适的值;
namelen:确定name缓冲区的长度。
返回值:成功返回0;失败返回SOCK_ERROR。
1.8 listen函数
int listen(SOCKET s,int backlog);
说明:
该函数用于将套接字设为监听模式,建立一个监听队列来接收客户端的连接请求。对于流式套接字,必须处于监听模式下才能够接收客户端套接字的连接。
backlog:可以接受连接的最大数量。
返回值:同上。
1.9 accept函数
SOCKET accept(SOCKET s,struct sockaddr FAR* addr,addrlen);
s:套接字对象,应处于监听状态;
addr:一个sockaddr in指针;
addrlen:用于接收参数addr的长度;
返回值:成功返回一个SOCKET对象,不成功返回一个INVALID_SOCKET对象。
1.9 connet函数
int connect(SOCKET s,const struct FAR* name,namelen);
s:一个套接字;
套接字s想要连接的主机地址和端口号;
name:name缓冲区长度;
返回值:成功0;失败SOCKET_ERROR。
1.10 send函数
int send(SOCKET s,const char FAR* buf,int len,int flags);
s:一个套接字对象;
buf:存放要发送数据的缓冲区;
len:缓冲区的长度;
flags:函数的调用方式,一般设置为NULL;
返回值:成功返回发送资料长度,失败返回SOCKET_ERROR。
1.11 recv函数
int recv(SOCKET s,char FAR* buf,int len,int flags);
说明:
s:一个套接字对象;
buf:接受数据的缓冲区;
len:缓冲区长度;
flags:解释同上。
返回值:成功返回接收资料长度,失败返回SOCKET_ERROR。
1.12 UDP方式
sendto:
int sendto(SOCKET s,const char FAR* buf,int len,int flags,
const struct sockaddr FAR* to,int tolen);
说明:
重复不解释
to(可选)指针,指向目的套接字的地址;
tolength:to所指的地址长度;
返回值,成功返回发送长度,失败SOCKET_ERROR。
recvfrom:
int recvform(SOCKET s,char FAR* buf,int len,int flags,
struct sockaddr FAR* from,int FAR* fromlen);
重复的不解释
from:(可选)指针,指向装有源地址的缓冲区;
fromlen:(可选)指针,指向from缓冲区长度;
返回值:成功返回接受长度,失败同上。
1.13 closesocket
closesocket(SOCKET s);
关闭套接字
1.14 WSACleanup
int WSACleanup(void);
释放Ws_32.dll动态链接库初始化时分配的资源。
1.15 过程图
2 源代码
2.1服务器端
server_tcp.cpp
#include <iostream>
#include <string>
#include <WinSock2.h>
#pragma comment(lib,"ws2_32.lib")//加载ws2_32.ddl
using namespace std;
int main(int argc, char argv[]) {
WORD sockVersion = MAKEWORD(2,2);//socket版本,用2.2版本
WSADATA wsadate; //WSADATA对象
SOCKET serverSocket; //服务器端套接字
SOCKET clientSocket; //客户端套接字
sockaddr_in sockAddr; //服务器端口地址
sockaddr_in clientAddr; //客户端端口
//初始化DLL
if (WSAStartup(sockVersion, &wsadate) != 0) {
return EXIT_FAILURE;
}
//创建套接字
serverSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (serverSocket == INVALID_SOCKET) {
cout << "创建套接字失败!"<<endl;
return EXIT_FAILURE;
}
//指定服务器IP地址和端口
sockAddr.sin_family = AF_INET; //使用IPV4地址
sockAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//具体的IP地址
sockAddr.sin_port = htons(2200); //具体的端口号
//绑定套接字
if(bind(serverSocket,(sockaddr*)&sockAddr,sizeof(sockAddr))==SOCKET_ERROR){
cout << "绑定出错!"<<endl;
return EXIT_FAILURE;
}
//进入监听状态,最多有10个连接
if (listen(serverSocket, 10) == SOCKET_ERROR) {
cout << "监听出错!"<<endl;
return EXIT_FAILURE;
}
int length = sizeof(clientAddr);
cout << "正在等待连接......"<<endl;
clientSocket = accept(serverSocket,(sockaddr*)&clientAddr,&length);
if (clientSocket == SOCKET_ERROR) {
cout << "连接出错!"<<endl;
return EXIT_FAILURE;
}
else {
cout << "建立一条连接:" << inet_ntoa(clientAddr.sin_addr) << endl;
}
//实现客户端与服务器端通信
while (1) {
//接收客户端消息
cout << "来自客户端的消息:" << endl;
const int datalength = 100; //假设收到的字符长度最大为100
char reciveClient[datalength]; //用来保存接收到的消息
int client_data = recv(clientSocket, reciveClient, datalength, NULL); //以下两行用来在接收到的字符数组最后加上一个字符串结束符 '\0'
reciveClient[client_data] = '\0'; //表示发送的字符串已经到了结尾
if (client_data > 0) { //打印接收到的消息
cout << "客户端:" << reciveClient << endl;
}
//设置出口
if (strcmp(reciveClient, "exit()") == 0) { //退出循环
break;
}
//向客户端发送消息
cout << "请写下您要发送的消息:" << endl;
string data; //用来暂时保存要发送的消息
getline(cin,data); //获取输入
const char* sentClient; //设置保存输入的字符数组
sentClient = data.c_str(); //保存输入至sentClient中
send(clientSocket, sentClient, data.size(), NULL); //发送给客户端
//设置循环出口
if (strcmp(sentClient, "exit()") == 0) { //退出循环
break;
}
}
//关闭工作
closesocket(serverSocket);
closesocket(clientSocket);
WSACleanup();
return 0;
}
2.2 客户端
client_tcp.cpp
#include <iostream>
#include <string>
#include <WinSock2.h>
#include <stdlib.h>
#pragma comment(lib,"ws2_32.lib")
using namespace std;
int main() {
WORD socketVersion = MAKEWORD(2, 2); //socket版本,用的是2.2版本
WSADATA data; //WSADATA对象
SOCKET client_socket; //声明定义客户端套接字,ipv4,流式套接字(tcp)
sockaddr_in sockAddr; //服务器端接口地址
//初始化DLL
if (WSAStartup(socketVersion, &data) != 0) {
return EXIT_FAILURE;
}
//创建客户端套接字
client_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (client_socket == INVALID_SOCKET) {
cout << "创建套接字失败!" << endl;
return EXIT_FAILURE;
}
//指定服务器端端口和地址
sockAddr.sin_family = AF_INET; //使用ipv4地址
sockAddr.sin_port = htons(2200); //2200端口
sockAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); //连接IP地址
//开始连接
if (connect(client_socket, (sockaddr*)&sockAddr, sizeof(sockAddr)) == SOCKET_ERROR) {
cout << "连接出错!" << endl;
return EXIT_FAILURE;
}
else {
cout << "连接成功!" << endl;
}
//实现与服务端通信
while (1) {
//向服务器端发送消息
cout << "请输入你要发送的信息:" << endl;
string data; //用来暂时保存获取输入
getline(cin, data); //从键盘获取输入
const char* sendServer; //用来保存输入
sendServer = data.c_str(); //从键盘获取的输入复制到sentServer中
send(client_socket, sendServer, data.size(), NULL); //发送消息
//设置循环出口
if (data == "exit()") {
break; //退出循环
}
const int datalength = 100; //假设允许发送消息的最大长度为100
char reciveServer[datalength]; //保存接收到的消息
int server_data = recv(client_socket, reciveServer, datalength, NULL); //以下两行用来在接收到的字符数组最后加上一个字符串结束符 '\0'
//表示发送的字符串已经到了结尾
reciveServer[server_data] = '\0';
//打印服务器端消息
if (server_data > 0) {
cout << "来自服务器端的消息:" << reciveServer << endl;
}
//设置循环出口
if (strcmp(reciveServer, "exit()") == 0) {
break; //退出循环
}
}
//关闭工作
closesocket(client_socket);
WSACleanup();
//system("pause");
return 0;
}
3 运行截图
4 遇到的问题
4.1 C4996’inet_ntoa’: Use inet_ntop() or InetNtop() instead or define _WINSOCK_D
解决办法:ALT+P+P打开属性页,见下图
4.2 如何同时运行的问题
解决办法:客户端服务器端都编译运行成功之后关闭VS,打开路径中的两个.exe后缀文件,运行即可,注意要先打开服务器端,再打开客户端。
4.3 一打开客户端客户端就闪退
解决办法:对于我个人来说,只是一个定义上出了毛病,导致客户端可以编译运行,但是会一直连接出错,并且点开.exe文件还会出现闪退的问题,原本客户端定义套接字我是这样定义的:声明定义在一起
SOCKET client_socket= socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
这样他就会一直闪退,解决办法也很直接,声明定义分开来就可以了:
SOCKET client_socket;
client_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);