1.socket编程的一些概念
socket作用:运行在计算机中的两个程序通过socket建立起一个通道,数据在通道中传输。socket提供了流(stream),是基于TCP协议,是一个有序、可靠、双向字节流的通道,传输数据不会丢失、不会重复、顺序也不会错乱。
TCP与UDP的区别:
1.TCP基于连接与UDP无连接
2.对系统资源的要求(TCP 较多,UDP 少)
3.UDP 程序结构较简单
4.流模式(TCP)与数据报模式(UDP)
TCP 保证数据正确性,UDP可能丢包
TCP 保证数据顺序,UDP 不保证
具体编程时的区别
1.socket的参数不同
2.UDP Server不需要调用 listen 和accept
3.UDP 收发数据用 sendto/recvfrom函数
4.TCp:地址信息在 connect/accept 时确定,UDP :在sendtorecvfrom函数中每次均 需指定地址信息5.UDP : shutdown 函数无效
socket中的一些名词解释:
多重协议支持: 通过SPI接口支持其他协议;
多重命名空间: 根据服务与主机名选择协议
重叠I/0模式: 增强I/0香叶量与提高性能
分散与聚合:从多个缓冲区发送与接收数据
有条件接受:有选择性地决定是否接受连接
套接字共享:多个进程共享一个套接字句柄
socket简单的通信流程
2.tcp/ip协议的基本知识
3.网络编程代码
1.头文件
c++网络编程有两个版本的头文件,第一版 <WinSock.h>,第二版 <WinSock2.h>,现在使用一般都是使用第二个版本,网络库也有两个版本,第一版 "wsock32.lib", 第二版 "ws2_32.lib",这两个版本都能用
#include<WinSock.h>//第一版
#include<WinSock2.h>//第二版
#pragma comment(lib,"wsock32.lib")//第一版 网络库
#pragma comment(lib,"ws2_32.lib")//第二版
2.WSAStartup()函数的使用
该函数的作用是用进程启动网络库(Winsock DLL),函数的原型
int WSAAPI WSAStartup(
[in] WORD wVersionRequested,
[out] LPWSADATA lpWSAData
);
WORD类型实际上是一个 unsigned short 类型,参数wVersionRequested需要指定网络库的版本号,参数lqWSAData是一个结构体类型,返回的各种版本信息会存入结构体中的成员。
函数如果执行成功会返回一个0,失败会返回错误信息,返回的错误信息有一些几类
错误代码 | 含义 |
---|---|
WSASYSNOTREADY | 基础网络子系统尚未准备好进行网络通信 |
WSAVERNOTSUPPORTED | 此特定 Windows 套接字实现不提供请求的 Windows 套接字支持版本。 |
WSAEINPROGRESS | 正在执行阻止 Windows 套接字 1.1 操作。 |
WSAEPROCLIM | 已达到 Windows 套接字实现支持的任务数的限制。 |
WSAEFAULT | lpWSAData 参数不是有效的指针。 |
代码样例
#include<stdio.h>
#include<WinSock2.h> //网络头文件 window socket 第二版
#pragma comment(lib,"ws2_32.lib")//包含网络库的名字, ws2_32.lib是网络库 名字 //不区分大小写
//#pragma comment(lib, "wsock32.lib") //第一版的库
int main()
{
//WSAstartup函数有两个参数,一个是 WORD 类型,一个是 LPWSADATA 结构体类型 在文档中 LP 开头的变量表示需要的地址变量
WORD wdVersion = MAKEWORD(2, 2);//选定版本号 这里表示2.2版本
//LPWSADATA和WSADATA*等价
//LPWSADATA lwq = malloc(sizeof(WSADATA));//创建一个堆区
WSADATA wdSockMsg;
int nRes = WSAStartup(wdVersion, &wdSockMsg);//有返回值,启动成功返回0
if (nRes != 0)
{//这里是5个错误码
switch (nRes)
{
case WSASYSNOTREADY:
printf("基础网络子系统尚未准备好进行网络通信");
break;
case WSAVERNOTSUPPORTED:
printf("此特定 Windows 套接字实现不提供请求的 Windows 套接字支持版本");
break;
case WSAEINPROGRESS:
printf("正在执行阻止 Windows 套接字 1.1 操作。 ");
break;
case WSAEPROCLIM:
printf("已达到 Windows 套接字实现支持的任务数的限制。");
break;
case WSAEFAULT:
printf("lpWSAData 参数不是有效的指针");
break;
}
}
//校验版本
//HIBYTE表示高位:是主版本,LOBYTE表示地位:是副版本
if (2 != HIBYTE(wdSockMsg.wVersion) || 2 != LOBYTE(wdSockMsg.wVersion))
{
//版本不对
WSACleanup();//关闭网络库函数
return 0;
}
system("pause");
return 0;
}
3.SOCKET函数的使用
1.socket介绍
2.socket()函数
该函数创建一个绑定到特定传输服务提供者的套接字,函数原型
SOCKET WSAAPI socket(
[in] int af,
[in] int type,
[in] int protocol
);
参数1 af:是地址的类型参数
参数名称 | 含义 | 例子 |
---|---|---|
AF_INET 2 | 表示IPV4的地址,4个字节,32位的地址 | 192.168.179.132 |
AF_INET6 23 | 表示IPV6的地址,16个字节,128位地址 | fh40::4fd5:cgya:85b:84a5%20 |
AF_BTH 32 | 蓝牙地址系列 | 6D:2D:BC:AA:8G:11 |
参数2 type:套接字类型
类型 | 含义 |
---|---|
SOCK_STREAM 1 | 一种套接字类型,使用OOB数据传输机制提供有序、可靠、双向、基于连接的字节流。这种套接字类型为Internet地址族(AF_INET或AF_INET6)使用传输控制协议(TCP)。 |
SOCK_DGRAM 2 | 一种支持数据报的套接字类型,数据报是固定(通常是小的)最大长度的无连接、不可靠的缓冲区。这种套接字类型使用用户数据报协议(UDP)作为Internet地址族(AF_INET或AF_INET6)。 |
SOCK_RAW 3 | 一种套接字类型,提供一个原始套接字,允许应用程序操作下一个上层协议头。要操作IPv4报头,必须在套接字上设置IP_HDRINCL套接字选项。要操作IPv6报头,必须在套接字上设置IPV6_HDRINCL套接字选项。 |
SOCK_RDM 4 | 提供可靠消息数据报的套接字类型。这种类型的一个例子是Windows中的实用通用多播(Pragmatic General Multicast, PGM)多播协议实现,通常称为可靠的多播编程。
|
SOCK_SEQPACKET 5 | 一种套接字类型,提供基于数据报的伪流包。 |
参数3 protocol:协议的类型
类型 | 含义 |
---|---|
IPPROTO_TCP 6 | 传输控制协议(TCP)。当af参数为AF_INET或AF_INET6,类型参数为SOCK_STREAM时,这个值是可能的。 |
IPPROTO_UDP 17 | 用户数据报协议(UDP)。当af参数为AF_INET或AF_INET6,类型参数为SOCK_DGRAM时,这个值是可能的。 |
IPPROTO_IGMP 2 | Internet组管理协议(IGMP)。当af参数为AF_UNSPEC、AF_INET或AF_INET6且类型参数为SOCK_RAW或未指定时,这个值是可能的。 |
IPPROTO_ICMP 1 | Internet控制报文协议(ICMP)。当af参数为AF_UNSPEC、AF_INET或AF_INET6且类型参数为SOCK_RAW或未指定时,这个值是可能的。 |
返回值
socket函数如果成功返回一个可用的socket,如果失败会返回 INVALID_SOCKET ,表示失败,可以调用函数 WSAGetLastError() 获取错误码。
注意:在最后一定要调用函数 closesocket() 函数销毁socket。
代码样例
//SOCKET的使用
SOCKET socketServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//三个参数 如果成功会返回一个可以的socket,
//在最后一定要销毁socket套接字
if (INVALID_SOCKET == socketServer)
{
int a = WSAGetLastError();//当socket函数绑定传输协议套接字失败,会调用该函数,返回错误码。
WSACleanup();
return 0;
}
closesocket(socketServer);//销毁套接字的函数
4.bind()函数使用
该函数将本地地址与套接字相关联。函数原型
int WSAAPI bind(
[in] SOCKET s,
[in] const sockaddr *name,
[in] int namelen
);
参数1 SOCKET:是socket()函数返回的值
参数2 name:是ip地址
参数3 namelen:空间大小
参数2 name 是 sockaddr类型,这是网络库中内置的结构体类型,如下
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;
sin_family是一个与socker函数中第一个参数一样的变量
sin_port表示指定的端口号
sin_addr表示指定的ip地址
返回值
函数执行成功会返回0,失败会返回一个SOCKET_ERROR,实际上是一个-1
bind函数使用实例
//bing函数的使用
struct sockaddr_in si;//定义一个结构体
si.sin_family = AF_INET;//需要和socket函数中的参数一致
si.sin_port = htons("12345");//指定的端口号
si.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//指定ip地址
bind(socketServer, (const struct sockaddr*)&si, sizeof(struct sockaddr_in));
5.listen()函数的使用
该函数将套接字置于侦听传入连接的状态(就是等待客户端连接),函数原型
int WSAAPI listen(
[in] SOCKET s,
[in] int backlog
);
参数1 s:是socket函数的返回值
参数2 backlog:挂起的连接队列的最大长度
返回值:连接成功返回0,失败返回SOCKET_ERROR
listen函数使用实例
//listen()函数的使用
//listen(socketServer, SOMAXCONN);
if (SOCKET_ERROR == listen(socketServer, SOMAXCONN))
{
//出错了
int a = WSAGetLastError();
//释放
closesocket(socketServer);
//释放网络库
WSACleanup();
}
6.accept函数使用
该函数允许在套接字上尝试传入连接,即把接收到的客户端信息创建成socket,函数原型
SOCKET WSAAPI accept(
[in] SOCKET s,
[out] sockaddr *addr,
[in, out] int *addrlen
);
参数1 s:是socket函数的返回值
参数2 addr:是sockaddr定义的结构体对象
参数3 addrlen:第一个参数的空间长度
返回值:连接成功返回给客户端包好的socket,失败返回 INVALID_SOCKET,表示无效
注意:该函数在没有客户端连接时,会一直阻塞,直到有客户端连接
accept函数代码样例
//accept()函数的使用
//创建客户端连接
struct sockaddr_in clientMsg;//定义sockaddr_in的结构体类型
int len = sizeof(clientMsg);//作为参数传入到accept函数
//accept(socketServer,NULL,NULL);//可以这样
SOCKET socketClient = accept(socketServer, (struct sockaddr*)&clientMsg, &len);//成功返回一个客户端socket
if (INVALID_SOCKET == socketClient)
{
//出错了
int a = WSAGetLastError();
//释放
closesocket(socketServer);
//释放网络库
WSACleanup();
}
7.recv()函数使用
该函数从连接的套接字或绑定的无连接套接字接收数据,即得到指定客户端发来的消息,本质是由协议本身去做的,也就是socket底层实现,函数原型
int WSAAPI recv(
[in] SOCKET s,
[out] char *buf,
[in] int len,
[in] int flags
);
参数1 s:是收到信息的socket
参数2 buf:收到信息时,内容的保存空间
参数3 len:每次接收信息的字节大小
参数4 flags:一般为0,表示在系统缓冲区中读完就删除缓冲,如果传入参数 MSG_WAITALL 表示接收到的信息必须是参数3 len的大小才可以执行,否则会被阻塞
返回值:如果执行没有错误,返回的是收到的字节个数,如果连接正常关闭返回值为0,如果出现错误返回值SOCKET_ERROR,使用WSAGetLastError()函数可以拿到错误码
recv函数代码实例
//recv()函数使用
char buf[1500] = { 0 };//作为收到信息的存储空间
int res = recv(socketClient, buf, 1499, 0);//执行成功会返回接收到信息的字节大小
if (res == 0)
{
printf("连接中断,客户端下线\n");
}
else if (SOCKET_ERROR == res)
{
//出错了
int a = WSAGetLastError();//拿到错误码
//需要根据实际情况处理
}
else
{
printf("%d %s\n", res, buf);
}
8.send()函数使用
该函数在连接的套接字上发送数据,即把要发送的数据发送给客户端,函数原型
int WSAAPI send(
[in] SOCKET s,
[in] const char *buf,
[in] int len,
[in] int flags
);
参数1 s:客户端socket
参数2 buf:要发送的字符数组
参数3 len:一次发送的字节个数
参数4 flags:一般为0
返回值:发送成功返回发送的字节个数,发送失败返回SOCKET_ERROR
send函数实例
//send()函数使用
if (SOCKET_ERROR == send(socketClient, "abcd\0qwer", sizeof("abcd\0qwer"), 0))//成功会返
回发送出去的字节个数,一次发送出去的字节个数应该在15000以内
{
//发送不成功
printf("发送出错\n");
int a = WSAGetLastError();//拿到错误码
//需要根据实际情况处理
}
9.connect()函数的使用
该函数在客户端中使用,建立与指定套接字的连接,即连接指定ip地址和端口号的服务器,函数原型
int WSAAPI connect(
[in] SOCKET s,
[in] const sockaddr *name,
[in] int namelen
);
参数1 s:socket函数返回的值
参数2 name:struct sockaddr定义的结构体对象
参数3 namelen:第二个参数所占用的空间长度
返回值:连接成功返回0,不成功返回SOCKET_ERROR,需要使用WSAGetLastError()获取错误码,在工具中的错误查找,可以找到具体原因
connect函数代码实例
//连接到服务器
struct sockaddr_in serverMsg;
serverMsg.sin_family = AF_INET;
serverMsg.sin_port = htons("12345");
serverMsg.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
if (SOCKET_ERROR == connect(socketClient, (struct sockaddr*)&serverMsg, sizeof(serverMsg)))
{
int a = WSAGetLastError();//获取错误码
printf("连接出错\n");
closesocket(socketClient);
WSACleanup();
return 0;
}
10.模型缺点
客户端与服务器连接后,数据的传送是你一发,我一发的,这样就就造成如果一方没有发送数据,另一方就会死等的情况,如果在等待数据的过程中又有其它的连接请求,会出现无法处理的情况。
11.tcp/ip代码
服务端代码
#define _CRT_SECURE_NO_WARNINGS
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<WinSock2.h> //网络头文件 window socket 第二版
#pragma comment(lib,"ws2_32.lib")//包含网络库的名字, ws2_32.lib是网络库 名字 //不区分大小写
//#pragma comment(lib, "wsock32.lib") //第一版的库
int main()
{
//第一步打开网络库
//WSAstartup函数有两个参数,一个是 WORD 类型,一个是 LPWSADATA 结构体类型 在文档中 LP 开头的变量表示需要的地址变量
WORD wdVersion = MAKEWORD(2, 2);//选定版本号 这里表示2.2版本
//LPWSADATA和WSADATA*等价
//LPWSADATA lwq = malloc(sizeof(WSADATA));//创建一个堆区
WSADATA wdSockMsg;
int nRes = WSAStartup(wdVersion, &wdSockMsg);//有返回值,启动成功返回0
if (nRes != 0)
{//这里是5个错误码
switch (nRes)
{
case WSASYSNOTREADY:
printf("基础网络子系统尚未准备好进行网络通信");
break;
case WSAVERNOTSUPPORTED:
printf("此特定 Windows 套接字实现不提供请求的 Windows 套接字支持版本");
break;
case WSAEINPROGRESS:
printf("正在执行阻止 Windows 套接字 1.1 操作。 ");
break;
case WSAEPROCLIM:
printf("已达到 Windows 套接字实现支持的任务数的限制。");
break;
case WSAEFAULT:
printf("lpWSAData 参数不是有效的指针");
break;
}
printf("打开网络库出错\n");
return 0;
}
//第二步 校验版本
if (2 != HIBYTE(wdSockMsg.wVersion) || 2 != LOBYTE(wdSockMsg.wVersion))//HIBYTE表示高位:表示主版本,LOBYTE表示地位:表示副版本
{
//版本不对
WSACleanup();//关闭网络库函数
printf("校验版本出错\n");
return 0;
}
//第三步 创建SOCKET
//SOCKET的使用
SOCKET socketServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//三个参数 如果成功会返回一个可以的socket,
//在最后一定要销毁socket套接字
if (INVALID_SOCKET == socketServer)
{
int a = WSAGetLastError();//当socket函数绑定传输协议套接字失败,会调用该函数,返回错误码。
WSACleanup();
printf("创建socket出错\n");
return 0;
}
//struct sockaddr_in si;
//si.sin_family = AF_INET;
//si.sin_port = htons(27015);
//struct in_addr s;//存放Ipv4地址的结构体
//inet_pton(AF_INET, "127.0.0.1", (void*)&si.sin_addr.S_un.S_addr);//进行转换
//第四步 绑定地址与端口号
//bing函数的使用
struct sockaddr_in si;//定义一个结构体
si.sin_family = AF_INET;//需要和socket函数中的参数一致
si.sin_port = htons(12332);//指定的端口号
si.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//指定ip地址
//bind(socketServer, (const struct sockaddr*)&si, sizeof(struct sockaddr_in));
//返回值,成功返回0,不成功返回SOCKET_ERROR实际上是一个-1
if (SOCKET_ERROR == bind(socketServer, (const struct sockaddr*)&si, sizeof(struct sockaddr_in)))
{
//出错了
int a = WSAGetLastError();
//释放
closesocket(socketServer);
//释放网络库
WSACleanup();
printf("绑定ip和端口号出错\n");
return 0;
}
//第五步 开始监听
//listen()函数的使用
//listen(socketServer, SOMAXCONN);
if (SOCKET_ERROR == listen(socketServer, SOMAXCONN))
{
//出错了
int a = WSAGetLastError();
//释放
closesocket(socketServer);
//释放网络库
WSACleanup();
printf("监听函数出错\n");
return 0;
}
//第六步 创建客户端socket/接受连接
//accept()函数的使用
//创建客户端连接
struct sockaddr_in clientMsg;//定义sockaddr_in的结构体类型
int len = sizeof(clientMsg);//作为参数传入到accept函数
//accept(socketServer,NULL,NULL);//可以这样
SOCKET socketClient = accept(socketServer, (struct sockaddr*)&clientMsg, &len);//成功返回一个客户端socket
if (INVALID_SOCKET == socketClient)
{
//出错了
int a = WSAGetLastError();
//释放
closesocket(socketServer);
//释放网络库
WSACleanup();
printf("连接函数出错\n");
return 0;
}
printf("服务器\n");
char buf[1500] = { 0 };//作为收到信息的存储空间
int res = recv(socketClient, buf, 1499, 0);//执行成功会返回接收到信息的字节大小
if (res == 0)
{
printf("连接中断,客户端下线\n");
}
else if (SOCKET_ERROR == res)
{
//出错了
int a = WSAGetLastError();//拿到错误码
//需要根据实际情况处理
}
else
{
printf("%d %s\n", res, buf);
}
if (SOCKET_ERROR == send(socketClient, "我是服务器,我收到了你的消息", sizeof("我是服务器,我收到了你的消息"), 0))//成功会返回发送出去的字节个数,一次发送出去的字节个数应该在15000以内
{
//发送不成功
printf("发送出错\n");
int a = WSAGetLastError();//拿到错误码
//需要根据实际情况处理
}
//循环一下
while (1)
{
//第七步 与客户端收发消息
//recv()函数使用
int res = recv(socketClient, buf, 1499, 0);//执行成功会返回接收到信息的字节大小
if (res == 0)
{
printf("连接中断,客户端下线\n");
}
else if (SOCKET_ERROR == res)
{
//出错了
int a = WSAGetLastError();//拿到错误码
//需要根据实际情况处理
}
else
{
printf("%d %s\n", res, buf);
}
scanf("%s", buf);
//send()函数使用
if (SOCKET_ERROR == send(socketClient, buf, strlen(buf), 0))//成功会返回发送出去的字节个数,一次发送出去的字节个数应该在15000以内
{
//发送不成功
printf("发送出错\n");
int a = WSAGetLastError();//拿到错误码
//需要根据实际情况处理
}
}
closesocket(socketServer);//销毁套接字的函数 释放成功返回0
closesocket(socketClient);
WSACleanup();//清理网络库 释放成功返回0
system("pause");
return 0;
}
客户端代码
#define _CRT_SECURE_NO_WARNINGS
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
int main()
{
//第一步 打开网络库
WORD wdVersion = MAKEWORD(2, 2);
WSADATA wdSocketMsg;
int nRes = WSAStartup(wdVersion, &wdSocketMsg);
if (nRes != 0)
{
printf("打开失败");
return 0;
}
//第二部 校验版本
if (HIBYTE(wdSocketMsg.wVersion) != 2 || LOBYTE(wdSocketMsg.wVersion) != 2)
{
//版本不对
WSACleanup();//关闭网络库
printf("版本出错\n");
return 0;
}
//第三部 创建socket
SOCKET socketClient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//还是服务器的socket
if (socketClient == INVALID_SOCKET)
{
//创建了无效的socket
int a = WSAGetLastError();//获取错误码
printf("创建出错\n");
WSACleanup();
return 0;
}
//第四步 连接到服务器
struct sockaddr_in serverMsg;
serverMsg.sin_family = AF_INET;
serverMsg.sin_port = htons(12332);
serverMsg.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
if (SOCKET_ERROR == connect(socketClient, (struct sockaddr*)&serverMsg, sizeof(serverMsg)))
{
int a = WSAGetLastError();//获取错误码
printf("连接出错\n");
closesocket(socketClient);
WSACleanup();
return 0;
}
printf("客户端\n");
if (SOCKET_ERROR == send(socketClient, "我是客户端", sizeof("我是客户端"), 0))//发送成功会返回发送的字节个数
{
//发送失败
printf("发送出错\n");
int a = WSAGetLastError();//获取错误码
}
//循环一下
while (1)
{
//第五步 与服务起收发消息
char buf[1500] = { 0 };
int res = recv(socketClient, buf, 1499, 0);
if (res == 0)
{
printf("发送中断,服务器断开连接\n");
}
else if (res == SOCKET_ERROR)
{
//连接出错
int a = WSAGetLastError();//获取错误码
printf("接收出错\n");
}
else
{
printf("%d %s\n", res, buf);
}
scanf("%s", buf);
if (SOCKET_ERROR == send(socketClient, buf, strlen(buf), 0))//发送成功会返回发送的字节个数
{
//发送失败
printf("发送出错\n");
int a = WSAGetLastError();//获取错误码
}
}
closesocket(socketClient);
WSACleanup();
system("pause");
return 0;
}