对于多字节数据在内存中有两种存储方式:
Little-endian:低字节在前,高字节在后;
Big-endian:高字节在前,低字节在后
网络协议在处理多字节整数时,采用的是高端字节序,在编程时一定要考虑主机字节顺序与网络字节顺序的相互转换。
1 WinSock基本
1.1 加载/卸载WinSock库
如果装载Winsock1,那么必须include<Winsock1.h>,同时建立与Wsock32.lib的链接关系。
如果装载Winsock2,那么必须include<Winsock2.h>,同时建立与Ws_32.lib的链接关系。
在调用一个Winsock函数之前,必须先加载一个Winsock的动态链接库:
intWSAStartup( WORD wVersionRequested, LPWSADATA lpWSAData )
参数wVersionRequested是Winsock的版本号,最新版本是2.2。高位字节指定副版本,低位字节指定主版本,可用宏MAKEWORD(x,y)(其中,x是高位字节,y是低位字节)。如果需要Winsock2.2,指定0x0202或使用宏MAKEWORD(2,2)。
WSAStartup加载的Winsock库的信息存放到参数lpWSAData指向的结构体WSADATA中,可以设置为NULL。
卸载Winsock库:
intWSACleanup(void);
注意:每次调用WSAStartup( ),都需要调用相应的WSACleanup()。
1.2 SOCKET介绍
Win32中套接字不同于文件描述符,是一个独立的类型SOCKET,由两个函数建立的:
SOCKETWSASocket( int af, int type, intprotocol,
LPWSAPROTOCOL_INFOlpProtocolInfo,
GROUP g, DWORD dwFlags );
SOCKETsocket( int af, int type, int protocol );
参数af是协议的地址家族;参数type是协议的套接字类型;参数protocol指定了协议字段。
协议
地址家族
套接字类型
协议字段
IP
AF_INET
TCP
SOCK_STREAM
IPPROTO_IP
UDP
SOCK_DGRAM
IPPROTO_UDP
Raw sockets
SOCK_RAW
IPPROTO_RAW
IPPROTO_ICMP
参数g始终为0,因为目前尚无支持套接字组的Winsock版本。
参数dwFlags可以指定标志:WSA_FLAG_OVERLAPPED(默认设置)
参数lpProtocolInfo用来指定创建套接字的类型,可以设置为NULL。
1.3 WinSock的地址信息
Winsock通过sockaddr_in结构来指定IP地址和服务端口信息:
structsockaddr
{
unsigned short sa_family; /* 地址协议族,一般为AF_INET*/
char sa_data[14]; /* 14字节的协议地址,包含该socket的IP地址和端口号 */
};
另外还有一种结构类型sockaddr_in:
structsockaddr_in
{
short int sin_family; /* 地址族 */
unsigned short int sin_port; /* 网络字节顺序的端口号*/
struct in_addr sin_addr; /* IP地址 */
unsigned char sin_zero[8]; /* 填充0 以保持与结构sockaddr同样大小 */
};
sockaddr_in结构使用更为方便,sockaddr_in.sin_zero应该用bzero( )或memset( )将其置为0。sockaddr_in的指针和sockaddr的指针可以通用。
structin_addr
{
unsigned long s_addr; /* 网络字节顺序的IP地址*/
};
可以将s_addr指定为htonl( INADDR_ANY ),表示的是由系统自己指定IP地址。
在选择端口sin_port时,必须特别小心:
1.0~1023由IANA控制,是为固定服务保留的;
2.1024~49151是IANA列出来的、已注册的端口,供普通用户的普通用户进程或程序使用;
3.49152~65535是动态或私用端口,IANA这些端口上没有注册服务,可自由使用。
1.4 地址格式转换函数
inet_addr( )可以把一个点式IP地址转换成一个网络字节顺序的IP地址:
unsignedlong inet_addr( const char FAR *cp );
inet_nota( )可以把一个网络字节顺序的IP地址转换成一个点式IP地址:
char*FAR inet_ntoa( struct in_addr in );
1.5 网络字节顺序转换函数
主机字节顺序---->网络字节顺序:
u_longhtonl( u_long hostlong );
intWSAHtonl( SOCKET s, u_long hostlong, u_long*lpnetlong );
u_shorthtons( u_short hostshort );
intWSAHtons( SOCKET s, u_short hostshort, u_short* lpnetshort );
网络字节顺序---->主机字节顺序:
u_longntohl( u_long netlong );
intWSANtohl( SOCKET s, u_long netlong, u_long* lphostlong );
u_shortntohs( u_short netshort );
intWSANtohs( SOCKET s, u_short netshort, u_short* lphostshort );
1.6 名字解析
structhostent
{
char FAR* h_name; // 主机名
char FAR FAR ** h_aliases; // 主机别名数组
short h_addrtype; // 主机地址类型
short h_length; // 主机地址长度(字节)
char FAR FAR ** h_addr_list; // 主机地址数组,其中的地址是网络字节顺序。
}
1.6.1 主机名---->主机地址
structhostent * FAR gethostbyname( const char* name );
HANDLEWSAAsyncGetHostByName( HWND hWnd, unsigned int wMsg,
const char* name, char* buf, int buflen );
WSAAsyncGetHostByNameAPI( )是gethostbyname( )的异步版,在结束时利用Windows消息向应用程序发出通知:
参数name可以是主机名,也可以是点式IP地址。
参数bWnd指定异步请求结束时,收到消息的窗口句柄。
参数wMsg是异步请求结束时,收到的窗口消息。
参数buf指向接收hostent数据的存储区。
参数buflen指定buf存储区的大小,推荐使用MAXGETHOSTSTRUCT。
1.6.2 主机地址---->主机名
structHOSTENT* FAR gethostbyaddr( const char* addr, int len, int type );
HANDLEWSAAsyncGetHostByAddr( HWND hWnd, unsignedint wMsg, const char* addr,
int len, int type, char* buf, int buflen );
WSAAsyncGetHostByAddrAPI( )是gethostbyaddr( )的异步版,在结束时利用Windows消息向应用程序发出通知:
参数addr是指向主机地址的指针,该主机地址按照网络字节顺序。
参数len指定参数addr的字节长度。
参数type为AF_INET。
1.7 获取Winsock函数的错误代码
intWSAGetLastError(void);
发生错误之后调用WSAGetLastError( ),就会返回发生错误的完整代码。
1.8 套接字的状态
套接字的初始状态都是CLOSED
套接字可以说是Winsock通讯的核心.Winsock通讯的所有数据传输,都是通过套接字来完成的,套接字包含了两个信息,一个是IP地址,一个是Port端口号,使用这两个信息,我们就可以确定网络中的任何一个通讯节点.
Client
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <iostream>
#include <string>
#include <winsock2.h> // 包含网络通信头文件
#pragma comment(lib, "ws2_32.lib")
#define PORT 8888
#define SERVER_IP "127.0.0.1"
#define BUFFER_SIZE 256
static const std::string kExitFlag = "-1";
int main1() {
// 初始化socket dll。
WORD winsock_version = MAKEWORD(2,2);
WSADATA wsa_data;
if (WSAStartup(winsock_version, &wsa_data) != 0) {
std::cout << "Failed to init socket dll!" << std::endl;
return 1;
}
SOCKET client_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (client_socket == INVALID_SOCKET) {
std::cout << "Failed to create server socket!" << std::endl;
return 2;
}
// 绑定IP和端口 及需要连接的目的IP,和端口
sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.S_un.S_addr = inet_addr(SERVER_IP);
// connect() 向服务器发起请求,服务器的IP地址和端口号保存在 sockaddr_in 结构体中。直到服务器传回数据后,connect() 才运行结束。
if (connect(client_socket, (LPSOCKADDR)&server_addr, sizeof(server_addr)) == SOCKET_ERROR) {
std::cout << "Failed to connect server!" << std::endl;
return 3;
}
char recv_data[BUFFER_SIZE] = {};
while (true) {
std::string data;
std::cout << "Input data: ";
std::cin >> data;
if (send(client_socket, data.c_str(), data.size(), 0) < 0) {
std::cout << "Failed to send data!" << std::endl;
break;
}
//接受目的主机 client
int ret = recv(client_socket, recv_data, BUFFER_SIZE, 0);
if (ret < 0) {
std::cout << "Failed to receive data!" << std::endl;
break;
}
std::cout << "Receive data from server: " << recv_data << std::endl;
if (data == kExitFlag) {
std::cout << "Exit!" << std::endl;
break;
}
}
closesocket(client_socket);
WSACleanup();
return 0;
}
Server
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <iostream>
#include <string>
#include <winsock2.h> // 包含网络通信头文件
#pragma comment(lib, "ws2_32.lib") // 程序在链接的时候将该库链接进去。隐式加载动态库。加载 ws2_32.dll。
// 动态链接库有两种加载方式:隐式加载和显示加载。
// 1.隐式加载又叫载入时加载,指在主程序载入内存时搜索DLL,并将DLL载入内存。隐式加载也会有静态链接库的问题,如果程序稍大,加载时间就会过长,用户不能接受。
// .lib 文件包含DLL导出的函数和变量的符号名,只是用来为链接程序提供必要的信息,以便在链接时找到函数或变量的入口地址;
// .dll 文件才包含实际的函数和数据。所以首先需要将 dllDemo.lib 引入到当前项目.
// 2.显式加载又叫运行时加载,指主程序在运行过程中需要DLL中的函数时再加载。显式加载是将较大的程序分开加载的,程序运行时只需要将主程序载入内存,软件打开速度快,用户体验好。
// 动态链接库的好处:在需要的时候加载动态链接库某个函数。
// 隐式链接的缺点:使用比较简单,在程序的其他部分可以任意使用函数,但是当程序访问十来个dll动态链接库的时候,此时如果都使用隐式链接的时候,
// 启动此程序的时候,这十来个动态链接库都需要加载到内存,映射到内存的地址空间,这就会加大进程的启动时间,而且程序运行过程中,只是在某个条件下使用某个函数,
// 如果使用隐式链接会造成资源的浪费。这样需要采用动态加载的方式。
#define PORT 8888
#define BUFFER_SIZE 256
static const std::string kExitFlag = "-1";
// 参考:http://c.biancheng.net/cpp/html/3030.html
int mains1() {
// 初始化socket dll。
// WinSock 规范的最新版本号为 2.2。
// //主版本号为2,副版本号为2,返回 0x0202。
// WSADATA保存Dll的信息。
WORD winsock_version = MAKEWORD(2,2);
WSADATA wsa_data;
if (WSAStartup(winsock_version, &wsa_data) != 0) {
std::cout << "Failed to init socket dll!" << std::endl;
return 1;
}
// 参数 AF_INET 表示使用 IPv4 地址,SOCK_STREAM 表示使用面向连接的数据传输方式,IPPROTO_TCP 表示使用 TCP 协议。
// AF 是“Address Family”的简写,INET是“Inetnet”的简写。AF_INET 表示 IPv4 地址,例如 127.0.0.1;AF_INET6 表示 IPv6 地址,例如 1030::C9B4:FF12:48AA:1A2B。
// 据传输方式,常用的有两种:SOCK_STREAM 和 SOCK_DGRAM。 SOCK_STREAM 表示面向连接的数据传输方式。 SOCK_DGRAM 表示无连接的数据传输方式。
// protocol 表示传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议。
SOCKET server_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (server_socket == INVALID_SOCKET) {
std::cout << "Failed to create server socket!" << std::endl;
return 2;
}
// 绑定IP和端口。
// IP地址和端口都保存在 sockaddr_in 结构体中。
//struct sockaddr_in {
// sa_family_t sin_family; //地址族(Address Family),也就是地址类型
// uint16_t sin_port; //16位的端口号
// struct in_addr sin_addr; //32位IP地址
// char sin_zero[8]; //不使用,一般用0填充
//};
//struct in_addr {
// in_addr_t s_addr; //32位的IP地址
//};
//struct sockaddr {
// sa_family_t sin_family; //地址族(Address Family),也就是地址类型
// char sa_data[14]; //IP地址和端口号
//};
sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.S_un.S_addr = INADDR_ANY;
// 为什么使用 sockaddr_in 而不使用 sockaddr.
// sockaddr 是一种通用的结构体,可以用来保存多种类型的IP地址和端口号,而 sockaddr_in 是专门用来保存 IPv4 地址的结构体。
// 正是由于通用结构体 sockaddr 使用不便,才针对不同的地址类型定义了不同的结构体。
//bind 绑定值本地某个地址端口
if (bind(server_socket, (LPSOCKADDR)&server_addr, sizeof(server_addr)) == SOCKET_ERROR) {
std::cout << "Failed to bind port!" << std::endl;
return 3;
}
// 监听。
// 第二个参数是 请求队列的长度。
// listen() 只是让套接字进入监听状态,并没有真正接收客户端请求,listen() 后面的代码会继续执行,直到遇到 accept()。
// accept() 会阻塞程序执行(后面代码不能被执行),直到有新的请求到来。
if (listen(server_socket, 10)) {
std::cout << "Failed to listen!" << std::endl;
return 4;
}
// 循环接收数据。
sockaddr_in client_addr;
int client_addr_len = sizeof(client_addr);
std::cout << "Wait for connecting..." << std::endl;
// 程序一旦执行到 accept() 就会被阻塞(暂停运行),直到客户端发起请求。
SOCKET client_socket = accept(server_socket, (SOCKADDR*)&client_addr, &client_addr_len);
if (client_socket == INVALID_SOCKET) {
std::cout << "Failed to accept!" << std::endl;
return 5;
}
std::cout << "Succeed to receive a connection: " << inet_ntoa(client_addr.sin_addr) << std::endl;
char recv_buf[BUFFER_SIZE] = {};
while (true) {
// 接收数据。
// 返回值是读取的字节数。没有内容时,等待请求。
int ret = recv(client_socket, recv_buf, BUFFER_SIZE, 0);
if (ret < 0) {
std::cout << "Failed to receive data!" << std::endl;
break;
}
std::cout << "Receive from Client: " << recv_buf << std::endl;
if (kExitFlag == recv_buf) {
std::cout << "Exit!" << std::endl;
break;
}
// 发送数据给客户端。
char* send_data = "Hello, Tcp Client!\n";
send(client_socket, send_data, strlen(send_data), 0);
}
// 关闭套接字。
closesocket(client_socket);
closesocket(server_socket);
// 释放dll。
WSACleanup();
return 0;
}
---------------------
转载:https://blog.csdn.net/u011726005/article/details/76944620