一、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
:
![图片[1]-C++中如何利用socket建立通信【基础篇】?-FancyPig's blog](https://www.cvv-goods.com/wp-content/uploads/2023/03/20220913064905115-1024x530-1.webp)
我们在23
行这里下断点,然后调试:
![图片[2]-C++中如何利用socket建立通信【基础篇】?-FancyPig's blog](https://www.cvv-goods.com/wp-content/uploads/2023/03/20220913065431342-1024x751-1.webp)
可以看到err
是0
,并且确实是用到了WinSock 2.0
,也就是说Winsock
启动成功。
2. 一些前置知识
由于36-44
行是检查版本的,因此可以直接去掉:
![图片[3]-C++中如何利用socket建立通信【基础篇】?-FancyPig's blog](https://www.cvv-goods.com/wp-content/uploads/2023/03/20220913065440203-1024x751-1.webp)
打开官方文档,搜索socket function
,可以看到需要传入三个参数:
![图片[4]-C++中如何利用socket建立通信【基础篇】?-FancyPig's blog](https://www.cvv-goods.com/wp-content/uploads/2023/03/20220913065523481-1024x578-1.webp)
第一个参数af
是指定地址族,有ipv4
、ipx
、AppleTalk
、网络基本输入输出系统、ipv6
以及蓝牙地址族;
第二个参数type
是套接字的类型,比如流格式、数据报等;
第三个参数protocol
则是所使用的协议,比如ICMP
、IGMP
、TCP
、UDP
这些。
而如果没有出错的话,则会返回创建的socket
的描述符,否则返回错误信息,具体报错关键字可以参考官方文档。
然后关于绑定套接字的话需要用到bind
,具体参考官方文档:
![图片[5]-C++中如何利用socket建立通信【基础篇】?-FancyPig's blog](https://www.cvv-goods.com/wp-content/uploads/2023/03/20220913065537106-1024x578-1.webp)
第一个参数是未绑定的套接字;第二个参数是指向套接字地址类型的指针;第三个参数是name
参数指向的值的长度,以字节为单位。
关于第二个参数的套接字地址类型,官方文档给出了讲解,我们关注IPV4
的这块:
![图片[6]-C++中如何利用socket建立通信【基础篇】?-FancyPig's blog](https://www.cvv-goods.com/wp-content/uploads/2023/03/20220913065754445.webp)
可以看到包含了地址族、端口、ip
地址等。
客户机监听的话需要用到listen
方法,可以参考官方文档:
![图片[7]-C++中如何利用socket建立通信【基础篇】?-FancyPig's blog](https://www.cvv-goods.com/wp-content/uploads/2023/03/20220913065803454-1024x578-1.webp)
第一个参数表示监听的socket
,一般是服务器的;第二个参数是待连接队列的最大长度。
然后如没出错的话就返回0
,如果出错了就返回对应的报错内容。
然后我们还需要处理连接请求,也就是accept
方法,具体参考官方文档:
![图片[8]-C++中如何利用socket建立通信【基础篇】?-FancyPig's blog](https://www.cvv-goods.com/wp-content/uploads/2023/03/20220913065816565-1024x578-1.webp)
第一个参数是监听者的套接字,一般是服务器的;第二个参数是监听者的套接字地址信息,一般是服务器的ip
地址;第三个参数是待连接队列的最大长度,一般就是iP
地址的长度。
返回值是接收者也就是客户机的描述符。
除了这些,还需要接收,可以参考官方文档:
![图片[9]-C++中如何利用socket建立通信【基础篇】?-FancyPig's blog](https://www.cvv-goods.com/wp-content/uploads/2023/03/20220913065551273-1024x578-1.webp)
第一个参数连接进来的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
上面运行服务器端:
![图片[10]-C++中如何利用socket建立通信【基础篇】?-FancyPig's blog](https://www.cvv-goods.com/wp-content/uploads/2023/03/20220913065636132-1024x493-1.webp)
在Linux
中运行起客户端:
![图片[11]-C++中如何利用socket建立通信【基础篇】?-FancyPig's blog](https://www.cvv-goods.com/wp-content/uploads/2023/03/20220913065647702-1024x336-1.webp)
发现成功接收:
![图片[12]-C++中如何利用socket建立通信【基础篇】?-FancyPig's blog](https://www.cvv-goods.com/wp-content/uploads/2023/03/20220913065659155-1024x529-1.webp)
三、编写一个利用socket
+ UDP
建立通信的实例
由于udp
的其中一个特点是无连接,也就是不需要建立连接即可通信,因此上面的代码里面的监听、接收的部分就可以去掉了。
首先需要改动的是socket
的创建需要变化成如下形式:
SOCKET my_server = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
对于tcp
,收发数据是recv
函数,而udp
则是recvfrom
函数。对于该函数,官方文档里面的描述是这样的:
![图片[13]-C++中如何利用socket建立通信【基础篇】?-FancyPig's blog](https://www.cvv-goods.com/wp-content/uploads/2023/03/20220913065716452-1024x481-1.webp)
和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;
}
![图片[14]-C++中如何利用socket建立通信【基础篇】?-FancyPig's blog](https://www.cvv-goods.com/wp-content/uploads/2023/03/20220913065728348-1024x598-1.webp)
暂无评论内容