C++中如何利用socket建立通信【基础篇】?

一、TCP/IP协议与Socket

  1. socket属于抽象层,也就是应用层和传输层中间的那个;通过对下层协议的封装以方便应用层的使用。
  2. TCP的三次握手是什么样的呢? 首先服务器处于监听状态,客户机请求连接服务器,首先发送一个同步序列,也就是SYN_i,进入同步发送状态,这就是第一次握手; 服务器收到这个同步序列之后,确认同步序列,并发送ACK_i+1 + SYN_j的数据包给客户机,进入同步接收状态,也就是SYN_RECV,这就是第二次握手; 客户机收到同步序列并确认后,进入连接状态,再次发送确认确认包ACK_J+1,然后客户端和服务器进入ESTABLISHED状态,这就是第三次握手。 其中,SYN(synchronize)是请求同步的意思,ACK是确认同步的意思。

socket中服务器和客户机间的通信是什么样的呢?

首先,服务器和客户机都需要创建socket,服务器需要通过绑定操作也就是bind来将本地地址和套接字相关联,然后服务器进入监听状态,等待客户机连接;这个部分就相当于三次握手。

然后服务器和客户机之间可以通过sendreceive来发送和接收信息。

二、编写一个利用socket + TCP建立通信的实例

1. 关于winsock2.h

参考官方文档,可以看到需要传入wVersionRequestedlpWSAData这两个参数,前者表示可以使用的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

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

图片[2]-C++中如何利用socket建立通信【基础篇】?-FancyPig's blog

可以看到err0,并且确实是用到了WinSock 2.0,也就是说Winsock启动成功。

2. 一些前置知识

由于36-44行是检查版本的,因此可以直接去掉:

图片[3]-C++中如何利用socket建立通信【基础篇】?-FancyPig's blog

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

图片[4]-C++中如何利用socket建立通信【基础篇】?-FancyPig's blog

第一个参数af是指定地址族,有ipv4ipxAppleTalk、网络基本输入输出系统、ipv6以及蓝牙地址族;

第二个参数type是套接字的类型,比如流格式、数据报等;

第三个参数protocol则是所使用的协议,比如ICMPIGMPTCPUDP这些。

而如果没有出错的话,则会返回创建的socket的描述符,否则返回错误信息,具体报错关键字可以参考官方文档。

然后关于绑定套接字的话需要用到bind,具体参考官方文档

图片[5]-C++中如何利用socket建立通信【基础篇】?-FancyPig's blog

第一个参数是未绑定的套接字;第二个参数是指向套接字地址类型的指针;第三个参数是name参数指向的值的长度,以字节为单位。

关于第二个参数的套接字地址类型,官方文档给出了讲解,我们关注IPV4的这块:

图片[6]-C++中如何利用socket建立通信【基础篇】?-FancyPig's blog

可以看到包含了地址族、端口、ip地址等。

客户机监听的话需要用到listen方法,可以参考官方文档

图片[7]-C++中如何利用socket建立通信【基础篇】?-FancyPig's blog

第一个参数表示监听的socket,一般是服务器的;第二个参数是待连接队列的最大长度。

然后如没出错的话就返回0,如果出错了就返回对应的报错内容。

然后我们还需要处理连接请求,也就是accept方法,具体参考官方文档

图片[8]-C++中如何利用socket建立通信【基础篇】?-FancyPig's blog

第一个参数是监听者的套接字,一般是服务器的;第二个参数是监听者的套接字地址信息,一般是服务器的ip地址;第三个参数是待连接队列的最大长度,一般就是iP地址的长度。

返回值是接收者也就是客户机的描述符。

除了这些,还需要接收,可以参考官方文档

图片[9]-C++中如何利用socket建立通信【基础篇】?-FancyPig's blog

第一个参数连接进来的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)); //把套接字和服务器进行绑定

这里我在本机进行试验,服务器是我的宿主机,IP192.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

Linux中运行起客户端:

图片[11]-C++中如何利用socket建立通信【基础篇】?-FancyPig's blog

发现成功接收:

图片[12]-C++中如何利用socket建立通信【基础篇】?-FancyPig's blog

三、编写一个利用socket + UDP建立通信的实例

由于udp的其中一个特点是无连接,也就是不需要建立连接即可通信,因此上面的代码里面的监听、接收的部分就可以去掉了。

首先需要改动的是socket的创建需要变化成如下形式:

SOCKET my_server = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);

对于tcp,收发数据是recv函数,而udp则是recvfrom函数。对于该函数,官方文档里面的描述是这样的:

图片[13]-C++中如何利用socket建立通信【基础篇】?-FancyPig's blog

tcprecv函数不同的是,第一个参数并不是客户机的socket,而是服务器的socket;第二到四个参数都是和recv函数一样的;第五个参数from是一个socket地址类型的指针,用来把保存接收数据包的源地址;第六个参数是地址长度。

{% message color:success size:medium icon:fa-brands fa-node-js title:试验环境说明(与前文不同!) %}
在本文中,我的服务器是Linux系统,ip92.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
© 版权声明
THE END
喜欢就支持一下吧
点赞10 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容