UDP/IP 与 TCP/IP的区别:
TCP/IP协议的特点:
面向连接的,可靠的,基于字节流的传输层协议。
UDP/IP协议的特点:
面向非连接的,不可靠的,基于数据报的传输层协议。
基本C/S模型
TCP/IP 服务端与客户端是 1对1关系;UDP/IP 服务端与客户端 可以是1对1,1对多 关系。
基本C/S模型中服务端代码:
UDP/IP:
是面向非连接的,所以不进行listen、accept;与客户端进行收发消息调用的函数是:recvfrom、sendto。
TCP/IP:
是面向连接的,要进行listen、accept;与客户端进行收发消息调用的函数是:recv、send。
实现步骤:
服务端
1.打开网络库
int WSAStartup(
WORD wVersionRequired,
LPWSADATA lpWSAData
);
2.检验版本
if (2 != HIBYTE(WSAData.wVersion) || 2 != LOBYTE(WSAData.wVersion))
3.创建SOCKET
SOCKET socket(
int af,
int type, // 填 SOCK_DGRAM
int protocol // 填 IPPROTO_UDP
);
4.绑定地址与端口号
int bind(
SOCKET s,
const sockaddr *addr,
int namelen
);
5.与客户端进行收消息:
int recvfrom(
SOCKET s,
char *buf,
int len,
int flags,
sockaddr *from,
int *fromlen
);
recvfrom函数:
作用:得到当前服务端接收到的消息
本质:复制 将协议缓存区的数据复制黏贴进自定义的buf
与recv函数一样是进行接收消息,不同的是recv是傻等,recvfrom是死等
参数1:服务端的socket
特点:
TCP是获取指定的,recv函数与客户端是1对1的关系 -- 傻等
UDP是无差别获取,recvfrom函数与客户端是1对多的关系 -- 死等
参数2:客户端消息的存储空间,也就是一个字符数组
广域网:各级路由器上的MTU最小值是576字节
tcp = 576 - 20(TCP包头) - 20(IP包头) = 536;
udp = 576 - 8(UDP包头) - 20(IP包头) = 548;
参数3:想要读取的字节个数, 参数2的字节数
参数4:数据的读取方式
0 从协议缓存区取一个数据报,然后就删除掉
MSG_PEEK 取出数据报,但是协议缓存内不删除,一直残留,导致影响后面的数据读取
MSG_OOB 带外数据
参数5:对方的IP地址端口号
此处结构体为相应的IP地址和端口号 struct sockaddr* sockClient; 填 &sockClient;
参数6:参数5结构体的大小 int len = sizeof(&sockClient); 填 &len;
6.与客户端进行发消息
int sendto(
SOCKET s,
const char *buf,
int len,
int flags,
const sockaddr *to,
int tolen
);
sendto函数:
作用:向目标发送数据报
本质:send函数将我们的数据复制黏贴进行系统的协议发送缓冲区,计算机伺机发出去
参数1:当前服务端的socket(自己的socket)
参数2:给接收方发送的字节串 UDP统一字节数 548
参数3:参数2的大小(字节个数 548)
参数4:填0
参数5:对方IP地址与端口号结构体
参数6:参数5的大小
客户端
与服务端代码基本一致,客户端不进行调用bind地址与端口号,因为服务端IP地址与端口号是固定的,所以只需创建服务端的地址与端口号结构体,在sendto 函数中参数5传递就可以了
struct sockaddr_in sockServer;
服务端代码:
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
/*
UDP/IP (User Datagram Protocol/Internet Protocol) 用户数据报协议
TCP/IP 协议的特点:
面向连接的,可靠的,基于字节流的传输层协议。
UDP/IP 协议的特点:
面向非连接的,不可靠的,基于数据报的传输层协议。
基本C/S模型中服务端代码:
UDP/IP 与 TCP/IP 的区别:
UDP/IP:
是面向非连接的,不用进行listen(进行监听)、不进行accept(接收连接)
与客户端进行收发消息调用的是 recvfrom、sendto函数
TCP/IP:
是面向连接的,进行listen(进行监听)、进行accept(接收连接 本质创建客户端socket)
与客户端进行收发消息调用的是 recv、send函数
*/
// 1.打开网络库 2.校验版本号
void OpenAndCheckWSAStartup();
// 3.创建服务端socket
SOCKET Sock_Server();
// 全局声明要创建 创建服务端socket
SOCKET socketServer;
// 4.绑定地址与端口号
void Bind(SOCKET sock);
// 5.与客户端进行发收数据
void RecvFromAndSendTo(SOCKET sock);
// 6.监视关闭窗口按钮函数所需的参数1 回调函数
BOOL WINAPI fun(DWORD CtrlType)
{
switch (CtrlType)
{
case CTRL_CLOSE_EVENT:
// 关闭socket
closesocket(socketServer);
// 关闭网络库
WSACleanup();
break;
}
return TRUE;
}
int main()
{
// 1.打开网络库 2.校验版本号
OpenAndCheckWSAStartup();
// 3.创建服务端socket
socketServer = Sock_Server();
// 4.绑定地址与端口号
Bind(socketServer);
// 5.与客户端进行发收数据
RecvFromAndSendTo(socketServer);
// 6.监视关闭窗口
SetConsoleCtrlHandler(fun, TRUE);
// 关闭socket
closesocket(socketServer);
// 关闭网络库
WSACleanup();
system("pause");
return 0;
}
// 1.打开网络库 2.校验版本号
void OpenAndCheckWSAStartup()
{
// 定义WSAStartup函数中参数1 所需的版本号
WORD wVersionRequestd = MAKEWORD(2, 2); // 设置2.2版本号
// 声明WSAStartup函数中参数1 所需的结构体
WSADATA WSAData;
// 打开网络库
int nRet = WSAStartup(wVersionRequestd, &WSAData);
if (0 != nRet)
{
//出错了
int a = WSAGetLastError();
printf("网络库打开失败 %d\n", a);
return;
}
// 检验版本号
if (2 != HIBYTE(WSAData.wVersion) || 2 != LOBYTE(WSAData.wVersion))
{
printf("校验版本号错误\n");
// 关闭网络库
WSACleanup();
return;
}
}
// 3.创建服务端socket
SOCKET Sock_Server()
{
// 创建服务端 socket
SOCKET sock_server = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
// 判断服务端socket是否成功创建
if (INVALID_SOCKET == sock_server)
{
//出错了
int a = WSAGetLastError();
printf("创建服务端socket失败,获取的错误码为 %d\n", a);
//关闭网络库
WSACleanup();
return 0;
}
return sock_server;
}
// 4.绑定地址与端口号
void Bind(SOCKET sock)
{
// 声明bind函数参数2所需的 结构体
struct sockaddr_in sockMsg;
// 给结构体成员赋值
sockMsg.sin_family = AF_INET;
sockMsg.sin_port = htons(12345);
sockMsg.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
// 绑定地址与端口号
int nRet = bind(sock, (struct sockaddr*)&sockMsg, sizeof(sockMsg));
// 判断是否成功绑定地址与端口号
if (SOCKET_ERROR == nRet)
{
//出错了
int a = WSAGetLastError();
printf("绑定地址与端口号失败,获取的错误码为 %d\n", a);
//关闭socket
closesocket(sock);
//关闭网络库
WSACleanup();
return;
}
}
// 5.与客户端进行发收数据
void RecvFromAndSendTo(SOCKET sock)
{
/*int recvfrom(
SOCKET s,
char *buf,
int len,
int flags,
sockaddr *from,
int *fromlen
);
recvfrom函数:
作用:得到当前服务端接收到的消息
本质:复制 将协议缓存区的数据复制黏贴进自定义的buf
与recv函数一样是进行接收消息,不同的是recv是傻等,recvfrom是死等
参数1:服务端的socket
特点:
TCP是获取指定的,recv函数与客户端是1对1的关系 -- 傻等
UDP是无差别获取,recvfrom函数与客户端是1对多的关系 -- 死等
参数2:客户端消息的存储空间,也就是一个字符数组
广域网:各级路由器上的MTU最小值是576字节
tcp = 576 - 20(TCP包头) - 20(IP包头) = 536;
udp = 576 - 8(UDP包头) - 20(IP包头) = 548;
参数3:想要读取的字节个数, 参数2的字节数
参数4:数据的读取方式
0 从协议缓存区取一个数据报,然后就删除掉
MSG_PEEK 取出数据报,但是协议缓存内不删除,一直残留,导致影响后面的数据读取
MSG_OOB 带外数据
参数5:对方的IP地址端口号
此处结构体为相应的IP地址和端口号 struct sockaddr* sockClient; 填 &sockClient;
参数6:参数5结构体的大小 int len = sizeof(&sockClient); 填 &len;
*/
/*int sendto(
SOCKET s,
const char *buf,
int len,
int flags,
const sockaddr *to,
int tolen
);
sendto函数:
作用:向目标发送数据报
本质:send函数将我们的数据复制黏贴进行系统的协议发送缓冲区,计算机伺机发出去
参数1:当前服务端的socket(自己的socket)
参数2:给接收方发送的字节串 UDP统一字节数 548
参数3:参数2的大小(字节个数 548)
参数4:填0
参数5:对方IP地址与端口号结构体
参数6:参数5的大小
*/
// 循环与客户端进行收发消息
while (1)
{
// 1.收消息
// 定义recvfrom 函数所需的接收字节数组
char buf[548] = { 0 };
// 声明recvfrom函数中参数5所需的 结构体相应的IP地址与端口号
struct sockaddr sockClient; // 对方的IP地址与端口号
// 定义recvfrom函数中参数6所需的 参数5结构体的大小
int len = sizeof(sockClient);
int nRet = recvfrom(sock, buf, 548, 0, &sockClient, &len);
// 判断是否成功接收消息
if (SOCKET_ERROR == nRet)
{
// 出错了
int a = WSAGetLastError();
printf("接收消息失败,获取的错误码为:%d\n", a);
continue;
}
// 进行接收消息
printf("client say: %s\n", buf);
// 2.发消息
int nSet = sendto(sock, "send ok", sizeof("send ok"), 0, &sockClient, sizeof(sockClient));
// 判断是否成功发送消息
if (SOCKET_ERROR == nSet)
{
// 出错了
int a = WSAGetLastError();
printf("发送消息失败,获取的错误码为:%d\n", a);
continue;
}
}
}
客户端代码:
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
/*
UDP/IP 基本C/S模型 客户端:
与 UDP/IP 基本C/S模型 服务端代码的区别:
代码基本一致,只有bind 函数有区别
服务端需要进行绑定地址与端口号,因为服务端是一对多
客户端只需要创建服务端地址与端口结构体,与服务端发送数据报时,
直接传递的是创建的结构体就可以了,不用进行绑定,因为服务端的ip与端口号固定不变的
*/
// 1.打开网络库 2.校验版本号
void OpenAndCheckWSAStartup();
// 3.创建服务端socket
SOCKET Sock_Server();
// 全局声明要创建 创建客户端socket
SOCKET socketClient;
// 4.与客户端进行发收数据
void SendToAndRecvFrom(SOCKET sock);
int main()
{
// 1.打开网络库 2.校验版本号
OpenAndCheckWSAStartup();
// 3.创建服务端socket
SOCKET socketClient = Sock_Server();
// 4.与客户端进行发收数据
SendToAndRecvFrom(socketClient);
//释放socket
closesocket(socketClient);
//清理网络库
WSACleanup();
system("pause");
return 0;
}
// 1.打开网络库 2.校验版本号
void OpenAndCheckWSAStartup()
{
// 定义WSAStartup函数中参数1 所需的版本号
WORD wVersionRequestd = MAKEWORD(2, 2); // 设置2.2版本号
// 声明WSAStartup函数中参数1 所需的结构体
WSADATA WSAData;
// 打开网络库
int nRet = WSAStartup(wVersionRequestd, &WSAData);
if (0 != nRet)
{
//出错了
int a = WSAGetLastError();
printf("网络库打开失败 %d\n", a);
return;
}
// 检验版本号
if (2 != HIBYTE(WSAData.wVersion) || 2 != LOBYTE(WSAData.wVersion))
{
printf("校验版本号错误\n");
// 关闭网络库
WSACleanup();
return;
}
}
// 3.创建服务端socket
SOCKET Sock_Server()
{
// 创建服务端 socket
SOCKET sock_server = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
// 判断服务端socket是否成功创建
if (INVALID_SOCKET == sock_server)
{
//出错了
int a = WSAGetLastError();
printf("创建服务端socket失败,获取的错误码为 %d\n", a);
//关闭网络库
WSACleanup();
return 0;
}
return sock_server;
}
// 4.与客户端进行发收数据
void SendToAndRecvFrom(SOCKET sock)
{
/*int recvfrom(
SOCKET s,
char *buf,
int len,
int flags,
sockaddr *from,
int *fromlen
);
recvfrom函数:
作用:得到当前服务端接收到的消息
本质:复制 将协议缓存区的数据复制黏贴进自定义的buf
与recv函数一样是进行接收消息,不同的是recv是傻等,recvfrom是死等
参数1:客户端的socket
特点:
TCP是获取指定的,recv函数与客户端是1对1的关系 -- 傻等
UDP是无差别获取,recvfrom函数与客户端是1对多的关系 -- 死等
参数2:客户端消息的存储空间,也就是一个字符数组
广域网:各级路由器上的MTU最小值是576字节
tcp = 576 - 20(TCP包头) - 20(IP包头) = 536;
udp = 576 - 8(UDP包头) - 20(IP包头) = 548;
参数3:想要读取的字节个数, 参数2的字节数
参数4:数据的读取方式
0 从协议缓存区取一个数据报,然后就删除掉
MSG_PEEK 取出数据报,但是协议缓存内不删除,一直残留,导致影响后面的数据读取
MSG_OOB 带外数据
参数5:对方的IP地址端口号
此处结构体为相应的IP地址和端口号 struct sockaddr* sockClient; 填 &sockClient;
参数6:参数5结构体的大小 int len = sizeof(&sockClient); 填 &len;
*/
/*int sendto(
SOCKET s,
const char *buf,
int len,
int flags,
const sockaddr *to,
int tolen
);
sendto函数:
作用:向目标发送数据报
本质:send函数将我们的数据复制黏贴进行系统的协议发送缓冲区,计算机伺机发出去
参数1:当前客户端的socket(自己的socket)
参数2:给接收方发送的字节串 UDP统一字节数 548
参数3:参数2的大小(字节个数 548)
参数4:填0
参数5:对方IP地址与端口号结构体
参数6:参数5的大小
*/
// 声明sendto函数中参数5所需的 服务端地址与端口号结构体
struct sockaddr_in sockServer;
// 给服务端地址与端口号结构体成员赋值
sockServer.sin_family = AF_INET;
sockServer.sin_port = htons(12345);
sockServer.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
// 循环与客户端进行收发消息
while (1)
{
// 定义要发送的字节数组
char strBuf[548] = { 0 };
// 手动发送
scanf_s("%s", strBuf, 548);
// 设置输入0时,将关闭客户端
if ('0' == strBuf[0])
{
break;
}
// 1.发消息
int nSet = sendto(sock, strBuf, sizeof(strBuf), 0, (struct sockaddr*)&sockServer, sizeof(sockServer));
// 判断是否成功发送消息
if (SOCKET_ERROR == nSet)
{
// 出错了
int a = WSAGetLastError();
printf("发送消息失败,获取的错误码为:%d\n", a);
continue;
}
// 2.收消息
// 定义recvfrom 函数所需的接收字节数组
char buf[548] = { 0 };
// 声明recvfrom函数中参数5所需的 结构体相应的IP地址与端口号
struct sockaddr sockClient; // 对方的IP地址与端口号
// 定义recvfrom函数中参数6所需的 参数5结构体的大小
int len = sizeof(sockClient);
int nRet = recvfrom(sock, buf, 548, 0, &sockClient, &len);
// 判断是否成功接收消息
if (SOCKET_ERROR == nRet)
{
// 出错了
int a = WSAGetLastError();
printf("接收消息失败,获取的错误码为:%d\n", a);
continue;
}
// 进行接收消息
printf("Server say: %s\n", buf);
}
}
程序运行结果: