C++中如何利用socket建立通信【基础篇】?
作者:Flemington | 发布时间: | 更新时间:
一、TCP/IP协议与Socket
socket属于抽象层,也就是应用层和传输层中间的那个;通过对下层协议的封装以方便应用层的使用。TCP的三次握手是什么样的呢? 首先服务器处于监听状态,客户机请求连接服务器,首先发送一个同步序列,也就是SYN_i,进入同步发送状态,这就是第一次握手; 服务器收到这个同步序列之后,确认同步序列,并发送ACK_i+1 + SYN_j的数据包给客户机,进入同步接收状态,也就是SYN_RECV,这就是第二次握手; 客户机收到同步序列并确认后,进入连接状态,再次发送确认确认包ACK_J+1,然后客户端和服务器进入ESTABLISHED状态,这就是第三次握手。 其中,SYN(synchronize)是请求同步的意思,ACK是确认同步的意思。
那socket中服务器和客户机间的通信是什么样的呢?
首先,服务器和客户机都需要创建socket,服务器需要通过绑定操作也就是bind来将本地地址和套接字相关联,然后服务器进入监听状态,等待客户机连接;这个部分就相当于三次握手。
然后服务器和客户机之间可以通过send和receive来发送和接收信息。
二、编写一个利用socket + TCP建立通信的实例
1. 关于winsock2.h库
参考官方文档,可以看到需要传入wVersionRequested和lpWSAData这两个参数,前者表示可以使用的Windows套接字规范的最高版本,其中高位字节指定次要版本号; 低位字节指定主要版本号;后者表示一个指向WSADATA数据结构的指针,用于接收Windows套接字实现的详细信息。
官方给出了这么一段例子:
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
// Need to link with Ws2_32.lib
#pragma comment(lib, "ws2_32.lib")
int __cdecl main()
{
WORD wVersionRequested;
WSADATA wsaData;
int err;
/* Use the MAKEWORD(lowbyte, highbyte) macro declared in Windef.h */
wVersionRequested = MAKEWORD(2, 2);
err = WSAStartup(wVersionRequested, &wsaData);
if (err != 0) {
/* Tell the user that we could not find a usable */
/* Winsock DLL. */
printf("WSAStartup failed with error: %d\n", err);
return 1;
}
/* Confirm that the WinSock DLL supports 2.2.*/
/* Note that if the DLL supports versions greater */
/* than 2.2 in addition to 2.2, it will still return */
/* 2.2 in wVersion since that is the version we */
/* requested. */
if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) {
/* Tell the user that we could not find a usable */
/* WinSock DLL. */
printf("Could not find a usable version of Winsock.dll\n");
WSACleanup();
return 1;
}
else
printf("The Winsock 2.2 dll was found okay\n");
/* The Winsock DLL is acceptable. Proceed to use it. */
/* Add network programming using Winsock here */
/* then call WSACleanup when done using the Winsock dll */
WSACleanup();
}
利用WSAStartup这个函数传入两个参数,第一个参数利用MAKEWORD宏来确定参数的版本;第二个参数wsaData传入WSADATA类型的结构体变量的指针来获取启动信息。
运行该example:

我们在23行这里下断点,然后调试:

可以看到err是0,并且确实是用到了WinSock 2.0,也就是说Winsock启动成功。
2. 一些前置知识
由于36-44行是检查版本的,因此可以直接去掉:

打开官方文档,搜索socket function,可以看到需要传入三个参数:

第一个参数af是指定地址族,有ipv4、ipx、AppleTalk、网络基本输入输出系统、ipv6以及蓝牙地址族;
第二个参数type是套接字的类型,比如流格式、数据报等;
第三个参数protocol则是所使用的协议,比如ICMP、IGMP、TCP、UDP这些。
而如果没有出错的话,则会返回创建的socket的描述符,否则返回错误信息,具体报错关键字可以参考官方文档。
然后关于绑定套接字的话需要用到bind,具体参考官方文档:

第一个参数是未绑定的套接字;第二个参数是指向套接字地址类型的指针;第三个参数是name参数指向的值的长度,以字节为单位。
关于第二个参数的套接字地址类型,官方文档给出了讲解,我们关注IPV4的这块:

可以看到包含了地址族、端口、ip地址等。
客户机监听的话需要用到listen方法,可以参考官方文档:

第一个参数表示监听的socket,一般是服务器的;第二个参数是待连接队列的最大长度。
然后如没出错的话就返回0,如果出错了就返回对应的报错内容。
然后我们还需要处理连接请求,也就是accept方法,具体参考官方文档:

第一个参数是监听者的套接字,一般是服务器的;第二个参数是监听者的套接字地址信息,一般是服务器的ip地址;第三个参数是待连接队列的最大长度,一般就是iP地址的长度。
返回值是接收者也就是客户机的描述符。
除了这些,还需要接收,可以参考官方文档:

第一个参数连接进来的socket,一般是客户机的socket;第二个参数是指向接收数据的缓冲区的指针;第三个参数是接受数据的长度;第四个参数是功能标志,一般设为0;
如果没出错就返回接受到的字节数,出错了就返回相关错误代码。
3. 尝试编写代码,进行socket通信
知道了这些东西之后我们就可以改下我们的代码实现socket连接了:
首先需要创建一个socket,只需要这一行代码:
SOCKET my_server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
然后进行参数创建和绑定:
sockaddr_in my_server_addr;
my_server_addr.sin_family = AF_INET;
my_server_addr.sin_port = htons(6666); //htons负责把参数转化成TCP/IP的网络字节序;6666是服务器的端口。
my_server_addr.sin_addr.s_addr = inet_addr("192.168.44.1"); //这里填入服务器IP
bind(my_server, (sockaddr*)&my_server_addr, sizeof(my_server_addr)); //把套接字和服务器进行绑定
这里我在本机进行试验,服务器是我的宿主机,IP为192.168.44.1,客户机是我的kali虚拟机,ip地址为192.168.44.132。
然后进行监听的创建:
listen(my_server, 5);
然后创建客户机的socket:
sockaddr_in my_client_addr;
int my_client_length = sizeof(my_client_addr);
SOCKET my_client = accept(my_server, (sockaddr*)&my_client_addr,&my_client_length);
//在没有连接到来的时候,listen函数一直处于阻塞的状态。
然后是接收的部分:
char recvdata[1024] = { 0 };
recv(my_client,recvdata,1023,0); //设置1023是为了防止溢出
到此服务器部分的代码写完了,完整的就是:
#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#include<iostream>
#pragma comment(lib, "ws2_32.lib")
using namespace std;
int __cdecl main()
{
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD(2, 2);
err = WSAStartup(wVersionRequested, &wsaData);
if (err != 0) {
printf("WSAStartup failed with error: %d\n", err);
return 1;
}
SOCKET my_server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in my_server_addr;
my_server_addr.sin_family = AF_INET;
my_server_addr.sin_port = htons(6666); //htons负责把参数转化成TCP/IP的网络字节序;6666是服务器的端口。
my_server_addr.sin_addr.s_addr = inet_addr("192.168.44.1"); //这里填入服务器IP
bind(my_server, (sockaddr*)&my_server_addr, sizeof(my_server_addr)); //把套接字和服务器进行绑定
listen(my_server, 5);
cout << "正在监听..." << endl;
sockaddr_in my_client_addr;
int my_client_length = sizeof(my_client_addr);
SOCKET my_client = accept(my_server, (sockaddr*)&my_client_addr,&my_client_length); //在没有连接到来的时候,listen函数一直处于阻塞的状态。
char recvdata[1024] = { 0 };
recv(my_client,recvdata,1023,0); //设置1023是为了防止溢出
cout << recvdata <<endl;
WSACleanup();
return 0;
}
需要注意的是,以上代码是适用于Windows系统的,如果是Linux系统的话,则为:
#include<sys/socket.h>
#include<arpa/inet.h>
#include<iostream>
using namespace std;
int main(){
socklen_t my_server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in my_server_addr;
my_server_addr.sin_family = AF_INET;
my_server_addr.sin_port = htons(6666); //htons负责把参数转化成TCP/IP的网络字节序;6666是服务器的端口。
my_server_addr.sin_addr.s_addr = inet_addr("192.168.44.1"); //这里填入服务器IP
bind(my_server, (sockaddr*)&my_server_addr, sizeof(my_server_addr)); //把套接字和服务器进行绑定
listen(my_server, 5);
cout << "正在监听..." << endl;
sockaddr_in my_client_addr;
int my_client_length = sizeof(my_client_addr);
socklen_t my_client = accept(my_server, (sockaddr*)&my_client_addr,&my_client_length); //在没有连接到来的时候,listen函数一直处于阻塞的状态。
char recvdata[1024] = { 0 };
recv(my_client,recvdata,1023,0); //设置1023是为了防止溢出
cout << recvdata <<endl;
return 0;
}
接下来写客户机的代码。
#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#include<iostream>
#pragma comment(lib, "ws2_32.lib")
using namespace std;
int __cdecl main()
{
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD(2, 2);
err = WSAStartup(wVersionRequested, &wsaData);
if (err != 0) {
printf("WSAStartup failed with error: %d\n", err);
return 1;
}
SOCKET my_client = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in my_server_addr;
my_server_addr.sin_family = AF_INET;
my_server_addr.sin_port = htons(6666); //htons负责把参数转化成TCP/IP的网络字节序;6666是服务器的端口。
my_server_addr.sin_addr.s_addr = inet_addr("192.168.44.1"); //这里填入服务器IP
connect(my_client, (sockaddr*)&my_server_addr, sizeof(my_server_addr));
send(my_client, "这里是W01fh4cker,很高兴认识你!", sizeof("这里是W01fh4cker,很高兴认识你!"), 0);
WSACleanup();
return 0;
}
同样的这段代码在Linux上面应该这么写:
#include<sys/socket.h>
#include<arpa/inet.h>
#include<iostream>
using namespace std;
int main(){
socklen_t my_client = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in my_server_addr;
my_server_addr.sin_family = AF_INET;
my_server_addr.sin_port = htons(6666); //htons负责把参数转化成TCP/IP的网络字节序;6666是服务器的端口。
my_server_addr.sin_addr.s_addr = inet_addr("192.168.44.1"); //这里填入服务器IP
connect(my_client, (sockaddr*)&my_server_addr, sizeof(my_server_addr));
send(my_client, "这里是W01fh4cker,很高兴认识你!", sizeof("这里是W01fh4cker,很高兴认识你!"), 0);
}
我们在Windows上面运行服务器端:

在Linux中运行起客户端:

发现成功接收:

三、编写一个利用socket + UDP建立通信的实例
由于udp的其中一个特点是无连接,也就是不需要建立连接即可通信,因此上面的代码里面的监听、接收的部分就可以去掉了。
首先需要改动的是socket的创建需要变化成如下形式:
SOCKET my_server = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
对于tcp,收发数据是recv函数,而udp则是recvfrom函数。对于该函数,官方文档里面的描述是这样的:

和tcp的recv函数不同的是,第一个参数并不是客户机的socket,而是服务器的socket;第二到四个参数都是和recv函数一样的;第五个参数from是一个socket地址类型的指针,用来把保存接收数据包的源地址;第六个参数是地址长度。
{% message color:success size:medium icon:fa-brands fa-node-js title:试验环境说明(与前文不同!) %}
在本文中,我的服务器是Linux系统,ip为92.168.44.132;客户机为Windows系统。
{% endmessage %}
现在我们来改写一下服务端的代码:
#include<sys/socket.h>
#include<arpa/inet.h>
#include<iostream>
#include<errno.h>
using namespace std;
int main()
{
socklen_t my_server = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
sockaddr_in my_server_addr,my_client_addr;
my_server_addr.sin_family = AF_INET;
my_server_addr.sin_port = htons(1012); //htons负责把参数转化成TCP/IP的网络字节序;1012是服务器的端口。
my_server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
int result = bind(my_server, (sockaddr*) & my_server_addr, sizeof(my_server_addr)); //把套接字和服务器进行绑定
if (result == -1){
wprintf(L"[-]套接字绑定失败 :(,错误原因为: %u\n", errno);
}else{
cout << "[+]套接字绑定成功!" << endl;
}
char recvdata[1024] = { 0 };
cout << "[*] 开始监听..." << endl;
socklen_t my_client_addr_length = sizeof(my_client_addr);
while(1){
result = recvfrom(my_server, recvdata, 1023, 0, (sockaddr*)&my_client_addr, &my_client_addr_length);
if(result != 0){
cout << "[+] 接受成功!接受数据为: " << recvdata << endl;
}else{
cout << "[-] 接受失败 :(" << endl;
}
}
return 0;
}
再改写一下客户机的代码:
#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#include<iostream>
#pragma comment(lib, "ws2_32.lib")
using namespace std;
int main() {
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD(2, 2);
err = WSAStartup(wVersionRequested, &wsaData);
if (err != 0) {
printf("WSAStartup failed with error: %d\n", err);
return 1;
}
SOCKET my_client = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
sockaddr_in my_server_addr;
my_server_addr.sin_family = AF_INET;
my_server_addr.sin_port = htons(1012); //htons负责把参数转化成TCP/IP的网络字节序;1012是服务器的端口。
my_server_addr.sin_addr.s_addr = inet_addr("192.168.44.132"); //这里填入服务器IP
err = sendto(my_client, "This is W01fh4cker, nice to meet you!", sizeof("This is W01fh4cker, nice to meet you!"), 0, (sockaddr * ) & my_server_addr, sizeof(my_server_addr));
if (err == SOCKET_ERROR) {
wprintf(L"sendto failed with error: %d\n", WSAGetLastError());
closesocket(err);
WSACleanup();
return 1;
}
else {
cout << "发送成功!发送字符串长度为:" << err << endl;
}
WSACleanup();
return 0;
}
