网络编程一

简介

网络编程就是编程程序使两台联网的计算机相互交换数据.
两台计算机之间用什么传输数据呢? 首先需要物理连接,其次需要考虑如何编程数据传输软甲.

对于后者, 在Windows系统下, 操作系统会提供名为**“套接字”**的部件, 套接字就是操作系统提供网络数据传输功能的设备, 因此网络编程也成为套接字编程.

常用Windows下的API

Windows系统下定义在<winsock2.h>头文件, 同时需要加载套接字库。

#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")

加载套接字库WSAStartup

加载套接字库:
函数原型:

int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
//wVersionRequested: 程序员要用的Winsock版本信息
//lpWSAData: WSADATA结构体变量的地址
//返回值: 成功时返回0, 失败时返回非零的错误代码值

对于wVersionRequested参数, 因为Winsock存在多个版本, 应该准备WORD类型的(WORD是通过typedef声明定义的unsigned short类型)套接字版本信息, 传递给该参数. 例版本为1.2, 其中1为主版本号, 2是副版本号, 所以应该传递0x0201. 为什么不是0x0102? 这又涉及到主机字节顺序和网络字节顺序, 参考:

该参数可以借助MAKEWORD宏构建, 示例:

WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaDSOCKET_ERRORata) != 0)
{
	return -1;
}

释放套接字库资源WSACleanup

与上面加载套接字库对应的注销套接字库, 函数原型:

int WSACleanup(void);
//返回值: 成功时返回0, 失败时返回SOCKET_ERROR(定义为-1)

调用该函数时, Winsock相关库将归还给Windows操作系统, 无法再调用Winsock相关函数.

创建套接字socket

函数原型:

SOCKET socket(int af,int type, int protocol)
//af: 指明了协议族/域,通常AF_INET(IPV4)、AF_INET6(IPV6)、AF_LOCAL等;
//type: 套接口类型,主要SOCK_STREAM(TCP)、SOCK_DGRAM(UDP)、SOCK_RAW;
//protocol: 计算机间通信使用的协议信息, 一般取为0。
//返回值: 成功返回套接字句柄, 失败时返回INVALID_SOCKET(定义为0)

注意:

typedef unsigned int SOCKET;
typedef VOID* HANDLE;
//Linux系统中的文件描述符,在Windows系统中称为“句柄”, 只不过Windows中要区分文件句柄和套接字句柄,如上的typedef。
//为什么要SOCKET而不直接用整型变量接收, 是因为微软考虑到后续的扩展性才出此下策, 因此判断返回值时也最好使用INVALID_ERROR, 因为后续可能会更改.

关于这三个参数这里可以补充说明一下:

协议族(Protocol Family):

①AF_INET : IPv4互联网协议族

②AF_INET6: IPv6互联网协议族

③AF_LOCAL: 本地通信的UNIX协议族

套接字中实际采用的最终协议信息时通过第三个参数传递的,在指定的协议族范围内通过第一个参数决定第三个参数。

套接字类型

套接字类型是指套接字的数据传输方式,通过第二个参数传递。

已通过第一个参数传递协议族信息,还要决定数据传输方式?因为每个协议族中都存在多种数据传输方式。例:AF_INET(IPv4)协议族中有面向连接的TCP和面向无连接的UDP。

面向连接的套接字(SOCK_STREAM)

特点:

①一对一连接

②传输过程中数据不会消失

③数据按需传输

④传输的数据不存在数据边界(Boundary)

例:传输数据的计算机通过调用3次send函数传递了100个字节的数据,但接收数据的计算机仅通过1次recv函数的调用就接收了全部100个字节的数据。(附代码案例)

这是因为收发数据的套接字内部有缓冲(buffer),即字符数组。通过套接字传输的数据将保存到该字符数组中。因此,收到数据并不意味着马上调用read,只要收到的数据(加上buffer中已有数据)不超过buffer的容量,则可以在数据填充满缓冲后通过1次recv函数读取全部数据,也可以分多次recv函数调用(设置每次读取的最大字节数)。

综上,在面向连接的套接字中,recv和send的调用次数并无太大意义,所以说面向连接的套接字不存在数据边界。

套接字缓冲已满是否意味着数据丢失?

只要调用recv函数从缓冲中读取部分数据,则缓冲并不总是满的。但如果recv函数读取速度比接收数据慢,则缓冲有可能被填满。此时套接字无法再接收数据,但即使这样也不会发生数据丢失,因为传输端套接字将停止传输。

也就是说,面向连接的套接字会根据接收端的状态传输数据,如果传输出错还会提供重传服务。

面向消息的套接字(SOCK_DGRAM)

特性:不可靠的、无序的、以数据高速传输为目的的套接字。

协议的最终选择

socket函数的第三个参数,决定最终采用的协议。

前两个参数确定协议族和套接字数据传输方式还不足以决定采用的协议吗?

一般情况下,传递前两个参数即可创建所需套接字,所以大部分情况下可以向第三个参数传递0即可。

特殊情况:同一协议族中存在多个数据传输方式相同的协议。

例:假设AF_INET中以面向连接的数据的传输的方式有两个(实际上只有TCP一种),这时就需要通过第三个参数来最终确定了。

绑定IP地址和端口bind

函数原型:

int bind(SOCKET s, const struct SOCKADDR* name, int namelen);
//s: socket函数返回的套接字句柄
// name: SOCKADDR结构体变量的地址
//namelen: SOCKADDR结构体变量的长度
//返回值: 成功时返回0, 失败时返回SOCKET_ERROR(定义为-1)

关于结构体SOCKADDR和SOCKADDR_IN参考: 待更…

进入等待连接请求状态listen

int listen(SOCKET s, int backlog);
//s: socket函数的返回的套接字句柄, 即希望进入等待连接请求状态的套接字句柄.
//backlog: 等待连接请求队列的长度, 例传入5, 则表示最多5个连接请求进入队列
//返回值: 成功时返回0, 失败时返回SOCKET_ERROR(定义为-1)

关于等待连接请求状态参考: 待更…

受理客户端连接

SOCKET accept(SOCKET s, struct SOCKADDR* addr, int* addrlen);
//s : 服务器端的监听套接字
//addr : 服务器端的地址信息
//addrlen : SOCKADDR结构体变量的长度
//返回值 : 成功时返回连接套接字句柄, 失败时返回INVALID_SOCKET(定义为0)
//注意: 该函数会阻塞程序

两个IO函数

send

函数原型

int send(SOCKET s, const char* buf, int len, int flags);
//s : 表示数据传输对象连接的套接字(连接套接字)的句柄值
//buf : 保存待传输数据的缓冲地址值
//len : 要传输的字节数
//flags : 传输数据时用到的多种选项信息, 该参数目前设置为0, 表示不设置任何选项.
//返回值: 成功时返回传输的字节个数, 失败时返回SOCKET_ERROR(定义为-1)

recv

int recv(SOCKET s, const char* buf, int len, int flags);
//s : 表示数据传输对象连接的套接字(连接套接字)的句柄值
//buf : 保存接收数据的缓存地址值
//len : 能够接收的最大字节数
//flags : 接收数据时用到的多种选项信息, 目前置0
//返回值 : 成功时返回接收的字节数(收到EOF-请求关闭连接时为0), 失败时返回SOCKET_ERROR(定义为-1)
//注意: 该函数会阻塞程序

断开连接closesocket

int closesocket(SOCKET s);
//成功时返回0, 失败时返回SOCKET_ERROR(定义为-1)

客户端请求连接connect

int connect(SOCKET s, const struct SOCKADDR* addr, int addrlen);
//s : 通过socket函数创建的连接套接字, 后补充说明连接套接字和监听套接字.
//addr: 这里指定的服务器端的地址信息
//addrlen: 结构体变量长度
//成功时返回0, 失败时返回SOCKET_ERROR(定义为-1)

开发步骤

服务器端

①加载套接字库
②创建监听套接字
③绑定服务器端IP地址和端口
④进入连接请求等待状态
⑤受理客户端连接
⑥IO通信
⑦断开连接
⑧释放套接字库资源

服务器端代码

#include <iostream>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")

int main(int argc, char* argv[])
{
	//检查main函数参数个数, 输入参数为可执行文件名 和 服务器端 端口号
	if (argc != 2)
	{
		return -1;
	}

	//加载套接字库
	WSADATA wsaData;
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		return -1;
	}

	//创建监听套接字
	SOCKET  srvSock = socket(AF_INET, SOCK_STREAM, 0);
	//判断是否创建成功
	if (srvSock == INVALID_SOCKET)
	{
		//释放套接字库
		WSACleanup();
		return -1;
	}

	//为服务器绑定IP和端口, 通过参数传递的端口
	SOCKADDR_IN srvAddr;
	memset(&srvAddr, 0, sizeof(srvAddr));
	srvAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);	//参数INADDR_ANY可自动获取运行服务器端的计算机的IP地址
	srvAddr.sin_family = AF_INET;	//注意和创建套接字时指定的协议族一致, 否则后面会bind失败
	srvAddr.sin_port = htons(atoi(argv[1]));	//0-1023 这1024个端口已分配
	//绑定并判断释放绑定成功
	if (bind(srvSock, (SOCKADDR*)&srvAddr, sizeof(SOCKADDR)) == SOCKET_ERROR)
	{
		closesocket(srvSock);
		WSACleanup();
		return -1;
	}

	//监听:进入等待连接请求状态
	if (listen(srvSock, 5) == SOCKET_ERROR)
	{
		closesocket(srvSock);
		WSACleanup();
		return -1;
	}

	//保存客户端地址信息
	SOCKADDR_IN cltAddr;
	memset(&cltAddr, 0, sizeof(cltAddr));
	int cltAddrLen = sizeof(SOCKADDR_IN);
	//受理客户端连接
	SOCKET cltSock = accept(srvSock, (SOCKADDR*)&cltAddr, &cltAddrLen);
	//判断释放成功受理
	if (cltSock == INVALID_SOCKET)
	{
		closesocket(srvSock);
		WSACleanup();
		return -1;
	}

	//向客户端发送信息
	char sendBuf[] = "Hello World!";
	//注意这里参数是cltSock(连接套接字)
	send(cltSock, sendBuf, sizeof(sendBuf), 0);

	//最后释放系统资源
	closesocket(cltSock);
	closesocket(srvSock);
	WSACleanup();
	return 0;
}

客户端

①加载套接字库
②创建连接套接字
③向服务器端发送连接请求
④IO通信
⑤断开连接
⑥释放套接字库资源

客户端代码

#include <iostream>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")

#include <WS2tcpip.h>

int main(int argc, char* argv[])
{
	//检查main函数参数个数, 输入参数为可执行文件名 服务器IP地址 和 服务器端 端口号
	if (argc != 3)
	{
		return -1;
	}

	//加载套接字库
	WSADATA wsaData;
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		return -1;
	}

	//创建客户端连接套接字
	SOCKET cltSock = socket(AF_INET, SOCK_STREAM, 0);
	if (cltSock == INVALID_SOCKET)
	{
		WSACleanup();
		return -1;
	}

	//初始化服务器端地址信息
	SOCKADDR_IN srvAddr;
	memset(&srvAddr, 0, sizeof(srvAddr));
	//srvAddr.sin_addr.S_un.S_addr = inet_addr(argv[1]);	//vs2015版本以上使用新函数代替了.
	inet_pton(AF_INET, argv[1], &srvAddr.sin_addr.S_un.S_addr);
	srvAddr.sin_family = AF_INET;
	srvAddr.sin_port = htons(atoi(argv[2]));

	//向服务器发送连接请求
	if (connect(cltSock, (SOCKADDR*)&srvAddr, sizeof(SOCKADDR)) == SOCKET_ERROR)
	{
		closesocket(cltSock);
		WSACleanup();
		return -1;
	}

	char recvBuf[100] = { 0 };
	//成功连接接收服务器消息
	int recvLen = recv(cltSock, recvBuf, 100, 0);
	if (recvLen == -1)
	{
		//调用失败, 服务器程序关闭了.
	}
	//打印接收消息
	std::cout << recvBuf << std::endl;
	closesocket(cltSock);
	WSACleanup();
	return 0;
}

测试

win + r 打开命令行窗口, 切换到上面程序的可执行文件目录:
在这里插入图片描述

先运行服务器端程序(空格分隔参数):
在这里插入图片描述
再开一个cmd窗口运行客户端程序:
在这里插入图片描述
成功接收到服务器端消息.

不想用命令行窗口也可以使用在vs配置属性中给定程序输入参数:
服务器程序:
在这里插入图片描述
客户端程序:
在这里插入图片描述
配置解决方案属性启动多个项目并设置启动顺序:
在这里插入图片描述
同样输出:
在这里插入图片描述

总结

该程序以服务器以及客户端开发的大致步骤为目的, 确实是太简陋了…

后续会介绍上面程序中涉及到的其他API接口, 以及对各函数相应的错误处理以及重连实现

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值