C++ Socket 入门
Socket 入门
前置知识 :计算机网络基础(TCP/IP四层模型)
Socket 原意是“插座”,在计算机通信领域被翻译为“套接字”,以\(\{IP:Port\}\) 的形式表示。
Windows 与Linux 的Socket编程有一些小的区别,由于Unix系统中一切都是文件,网络连接也不例外,只要网络联通,一切的操作都是在操作文件,每个文件都会有一个文件描述符。与之相对应的,Windows下面的文件描述符就是句柄,利用句柄来操作Socket。本文总结了Windows的Socket编程
最常用的Socket主要是两种:
- 流式套接字(TCP) : SOCK_STREAM
- 数据在传输时不会丢失(可靠的)
- 数据按照顺序传输(先发送的先到达,后发送的后到达)
- 数据的发送和接受不是同步的(TCP以字节流的形式发送的特性)
- 数据包格式套接字(UDP) : SOCK_DGRAM
- 强调速度快而非顺序
- 传输的信息可能丢包也能损毁
- 限制每次传输的大小
- 数据的发送和接受是同步的
以上两种套接字的特性可以参考TCP协议与UDP协议
使用Socket通信的步骤
Server
- socket() 创建套接字
- bind() 套接字与\(\{IP:Port\}\) 进行绑定
- listen() 监听
- accept() 接受链接请求
- recv() 接受消息
- close() 关闭套接字
Client
- socket() 创建套接字
- connect() 向服务器发起连接
- send() 向服务器发送消息
- close() 关闭套接字
创建socket之前需要掌握的知识
动态库的加载
Windows 下的 socket 程序依赖 Winsock.dll 或 ws2_32.dll,必须提前加载。
#pragma comment (lib, "ws2_32.lib")
动态库的初始化
在使用Socket依赖的DDL之前,要先对其初始化,所用的函数是WSAStartup()
,含义是\(Windows~Socket~API~Start~Up\), 要用该函数来指明WinSock的版本。
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
WORD
表示是一个字,两个字节16bit。wVersionRequested
参数表示WinSock的版本号,我们可以用宏函数MAKEWORD
直接转换传入参数(版本号一般是最新的2.2)。
WSADATA
结构体存放有关动态库的信息,在函数执行完之后,会将信息放入到 lpWSAData
中
LP 表示指针 long point
综上,以下两行代码即可实现动态库的初始化:
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
创建套接字
Windows 下使用socket函数来创建套接字,返回SOCKET类型句柄
SOCKET socket(int af, int type, int protocol);
-
af 地址族(Address Family)
AF_INET,AF_INET6分别表示IPv4和IPv6,除了AF,也可以用PF(Protocol Family),BF_INET,BF_INET6 -
type 套接字类型: SOCK_STREAM,SOCK_DGRAM。流式套接字以及数据包式套接字
-
protocol 传输协议:IPPROTO_TCP, IPPROTO_UDP
Example:
SOCKET servSock = socket(AF_INET, SOCK_STREAM, 0);
第三个参数一般可以是0,前提是前面两个参数可以自行推断出第三个参数
绑定套接字 & 客户端与服务端连接
socket()
可以用来创建套接字,服务器要将套接字与\(\{IP:Port\}\) 进行绑定,之后,流经该指定IP的端口数据将由套接字处理。
int bind(SOCKET sock, const struct sockaddr *addr, int addrlen);
sock 为SOCKET句柄,addr 为 sockaddr 结构体指针变量,addrlen 为 addr 变量大小,可以用 sizeof 运算符计算得到。
这里有一个重要的结构体 sockaddr
, 该结构体内保存着IP地址类型以及地址和端口信息。由于不同的IP地址(IPv4 or IPv6),长度并不相同,为了方便传入地址信息,需要利用另外两个结构体。
Example:bind
// sockaddr_in 结构体用来存放地址信息
sockaddr_in sockAddr;
memset(&sockAddr, 0, sizeof(sockAddr)); //每个字节都用0填充
sockAddr.sin_family = PF_INET; //使用IPv4地址
sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址,由于127.0.0.1并不好存储,所以需要inet_addr进行转换
sockAddr.sin_port = htons(1234); //端口,需要用htons函数转换
// 将sockaddr_in* 类型强制转换为SOCKADDR* 类型
bind(servSock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));
上面的例子与IPv4的地址进行绑定,如果使用IPv6地址,需要将sockaddr_in 结构体换为:sockaddr_in6
sockAddr.sin_addr.s_addr 保存了IP地址,为什么不直接存在sin_addr里面,而要把s_addr装在sin_addr结构体里面? 大概是历史的原因
至此,服务端将socket与\(\{IP:Port\}\) 绑定成功,客户端可以通过connect 函数向服务端发起连接请求
int connect(SOCKET sock, const struct sockaddr *serv_addr, int addrlen);
Example: connect
//向服务器发起请求
sockaddr_in sockAddr;
memset(&sockAddr, 0, sizeof(sockAddr));
sockAddr.sin_family = AF_INET;
sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
sockAddr.sin_port = htons(1234);
connect(sock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));
接听与响应
服务端使用 listen 函数使套接字进入监听状态,在调用accept函数,以便随时响应连接请求
int listen(SOCKET sock, int backlog);
SOCKET accept(SOCKET sock, struct sockaddr *addr, int *addrlen);
backlog 为请求队列的长度,套接字处理客户端请求时,新的请求要放入到缓存队列中,该参数限制了队列的长度,如果设置为 SOMAXCONN ,则交给系统决定该长度
注意:listen() 只是让套接字处于监听状态,并没有接收请求。接收请求需要使用 accept() 函数。
accept 函数中的addr保存了客户端的IP和端口信息
Example: listen & accept
//进入监听状态
listen(servSock, 20);
//接收客户端请求
SOCKADDR clntAddr;
int nSize = sizeof(SOCKADDR);
SOCKET clntSock = accept(servSock, (SOCKADDR*)&clntAddr, &nSize);
数据的接受与发送
!!! 终于到了有趣的地方了,前面所做的一切都是为了现在
发送数据使用:
int send(SOCKET sock, const char *buf, int len, int flags);
接受数据使用:
int recv(SOCKET sock, char *buf, int len, int flags);
这两个函数很简单,参数一目了然(最后的flags先不必管),buf表示要发送的字符串信息,len表示其长度。sock表示对方的socket句柄
服务器在accept到客户端的请求后,会获取到客户端的socket句柄,所以如果在服务器端向客户端发送数据或者接收数据就用该句柄
客户端在connect之前,创建的socket正是服务器的\(\{IP:Port\}\),利用该SOCKET句柄向服务器发送数据或者接收数据
实现回声客户端
Server端:
#include <stdio.h>
#include <winsock2.h>
#pragma comment (lib, "ws2_32.lib") //加载 ws2_32.dll
#define BUF_SIZE 100
int main() {
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
//创建套接字
SOCKET servSock = socket(AF_INET, SOCK_STREAM, 0);
//绑定套接字
sockaddr_in sockAddr;
memset(&sockAddr, 0, sizeof(sockAddr)); //每个字节都用0填充
sockAddr.sin_family = PF_INET; //使用IPv4地址
sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
sockAddr.sin_port = htons(1234); //端口
bind(servSock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));
//进入监听状态
listen(servSock, 20);
//接收客户端请求
SOCKADDR clntAddr;
int nSize = sizeof(SOCKADDR);
SOCKET clntSock = accept(servSock, (SOCKADDR*)&clntAddr, &nSize);
while (true) {
char buffer[BUF_SIZE];
int strLen = recv(clntSock, buffer, BUF_SIZE, 0);
buffer[strLen] = '\0';
if (strcmp(buffer, "quit") == 0) break;
send(clntSock, buffer, strLen, 0);
}
//关闭套接字
closesocket(clntSock);
closesocket(servSock);
//终止 DLL 的使用
WSACleanup();
return 0;
}
Client 端
#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib") //加载 ws2_32.dll
#define BUF_SIZE 100
int main() {
//初始化DLL Windows Socket API DATA
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
//创建套接字
SOCKET sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
//向服务器发起请求
sockaddr_in sockAddr;
memset(&sockAddr, 0, sizeof(sockAddr)); //每个字节都用0填充
sockAddr.sin_family = PF_INET;
sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
sockAddr.sin_port = htons(1234);
connect(sock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));
while (true) {
char bufSend[BUF_SIZE] = { 0 };
printf("Input a string: ");
scanf("%s", bufSend);
send(sock, bufSend, strlen(bufSend), 0);
if (strcmp(bufSend, "quit") == 0) {
break;
}
char bufRecv[BUF_SIZE] = { 0 };
recv(sock, bufRecv, BUF_SIZE, 0);
//输出接收到的数据
printf("Message form server: %s\n", bufRecv);
}
//关闭套接字
closesocket(sock);
//终止使用 DLL
WSACleanup();
system("pause");
return 0;
}