Table of Contents
准备工作
Windows网络编程一般是指 Windows Socket
编程(winsocket),它从UNIX Socket
发展而来。进行Windows网络编程,首先需要添加依赖库WS2_32.lib
或 WSOCK_32.lib
,加载动态库ws2_32.dll,放入C:/Windows/System32。然后使用时在源文件中包含头文件:
#include <WinSock2.h>
// #include <MSWSOCK.h>
// #include <winsock.h>
说明:
有些接口已经弃用,采用新的接口,具体是哪些,后面会慢慢指出。
- 如何引入依赖库?
#pragma comment(lib, "ws2_32.lib"); // 源文件中添加
也可以在配置文件中添加:属性----链接器----输入----ws2_32.lib.
socket
socket
套接字是应用层到传输层的接口,表示一个连接的两端,每个端由IP地址和端口port组成,即socket是由两端点的ip和端口port组成的
。
-
套接字类型 SOCKET 定义
typedef unsigned int SOCKET; // 句柄
-
端口
端口是传输层的概念,每个端口对应一个 process 进程,因此一条连接表示一个进程与另一个进程建立联系。
-
套接字类型
一般使用两种套接字:TCP 流套接字,UDP 数据报套接字。前者提供可靠的、无重复的、有序的数据流服务,后者提供不可靠传输。
C/S模式
winsocket
一般采用C/S模式
- Server 端流程
1、初始化winsocket
2、建立socket
3、绑定服务端地址(bind)
4、开始监听(listen)
5、然后与客户端建立连接(accept)
6、然后与客户端进行通信(send, recv)
7、当通信完成以后,关闭连接
8、释放winsocket的有关资源
- Client 端流程
1、初始化winsocket
2、建立socket
3、与服务器进行连接(connect)
4、与服务器进行通信(send, recv)
5、当通信完成以后,关闭连接
6、释放winsocket占用的资源
话不多说,先上一段代码,再小段分析
源代码
源码亲测可以运行
服务端 Server.cpp
#include "pch.h"
#include <iostream>
#include <WinSock2.h>
//#include <MSWSock.h>
using namespace std;
// 指定依赖库路径
#pragma comment(lib, "ws2_32.lib")
int main()
{
WSADATA wsaData; // 声明一个结构体
SOCKET listeningSocket; // 声明一个监听句柄
SOCKET newConnection; // 声明一个连接句柄
SOCKADDR_IN serverAddr; // 端点结构体
SOCKADDR_IN clientAddr;
int port = 5150; // 端口号
// 初始化socket
WSAStartup(MAKEWORD(2,2), &wsaData);
// 创建监听socket相应客户端请求
listeningSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// 填写服务器地址信息
// port=5150
// ip 地址为INNADDR_ANY,注意使用 hton 将 IP地址转换为网络格式
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(port);
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
// 绑定监听端口
bind(listeningSocket, (SOCKADDR *)&serverAddr, sizeof(serverAddr));
// 开始监听,指定最大同时连接数
listen(listeningSocket, 5);
// 接受新连接
int clientAddrLen = sizeof(clientAddr);
newConnection = accept(listeningSocket, (SOCKADDR *)&clientAddr, &clientAddrLen);
// 新连接建立后,就可以开始通信了
cout << "========= new client has been connected ========" << endl;
//
// 这里放通信代码
//
// 通讯结束后,关闭连接
// 并关闭监听socket,然后退出程序
closesocket(newConnection);
closesocket(listeningSocket);
// 释放window socket dll 资源
WSACleanup();
return 0;
}
客户端 Client.cpp
#include "pch.h"
#include <WinSock2.h>
#include <iostream>
#include <WS2tcpip.h>
using namespace std;
int main()
{
WSADATA wsaData;
SOCKET clientSocket;
SOCKADDR_IN serverAddr;
int port = 5150;
// 加载 dll,初始化socket 2.2
WSAStartup(MAKEWORD(2, 2), &wsaData);
// 创建一个新连接
clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(port);
serverAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); // 与本机通信
// 向服务器发送连接请求
int result = connect(clientSocket, (SOCKADDR *)&serverAddr, sizeof(serverAddr));
if (result == 0)
{
cout << "client has connect to server..." << endl;
}
// 关闭套接字,释放资源
closesocket(clientSocket);
WSACleanup();
return 0;
}
源码分析
- WSAData 结构体
typedef struct WSAData {
WORD wVersion; // 版本号
WORD wHighVersion;
#ifdef _WIN64
unsigned short iMaxSockets;
unsigned short iMaxUdpDg;
char FAR * lpVendorInfo;
char szDescription[WSADESCRIPTION_LEN+1];
char szSystemStatus[WSASYS_STATUS_LEN+1];
#endif
} WSADATA, FAR * LPWSADATA;
- SOCKADDR_IN 结构体
typedef struct sockaddr_in {
USHORT sin_port;
IN_ADDR sin_addr;
CHAR sin_zero[8];
} SOCKADDR_IN;
- 服务端需要 bind 的原因
无连接(connect)的服务端、客户端和面向连接的服务端通过 bind 来配置本地信息;而有连接的客户端通过调用 connect 函数在socket 数据结构中保存本地和远端信息,不需要调用 bind()。
- 需要初始化 WASStartup()的原因
之所以需要初始化winsocket,是因为Winsock的服务是以动态连接库Winsock DLL形式实现的,所以必须先调用初始化函数(WSAStartup)对Winsock DLL进行初始化,协商Winsock的版本支持,并分配必要的资源; // 在Linux环境中不需要该初始化步骤。
数据传输
在建立起连接的基础上,发送数据可以用接口 send / WSASend
,接收数据可以用 recv / WSARecv
。
- 对于 send 而言,发送数据的长度一般有限制,因为缓冲区或者 TCP/IP 的窗口大小有所限制,所以需要根据窗口大小来设定发送数据的长度。
- 对于 recv 而言,流套接字是一个不间断的数据流,在读取它时,应用程序通常不会关心应该读取多少数据,如果所有消息长度都一样,这应该简单处理,如读取 1024 字节。
char recvBuff[2048];
int ret; // 读取的数据长度
int nLeft; // 剩余空间
int idx; // 缓冲区数组下标
nLeft = 1024;
idx = 0;
while (nLeft > 0)
{
ret = recv(socket1, &recvBuff[idx], nLfet, 0);
if (ret == SOCKET_ERROR){
// error 读取失败
std::cout << "Error when receive message.";
}
idx += ret;
nLeft -= ret;
}
- 如果接收的消息长度不同,则按照发送端的协议来通知接收端,告知接收端即将到来的消息长度多少;比如,在消息的前几个字节设定标记,表示数据长度。
关闭连接
数据传输完成,关闭套接字,释放资源。
shutdown(); // 中断连接
closeSocket(socket_name);
WASCleanup(); // 释放 dll