C++学习之网络通信Socket技术

一、Socket是什么

socket就是套接字的意思,用于描述地址和端口。应用程序通过socket向网络发出请求或者回应。

socket编程有三种:

  • 流式套接字(SOCK_STREAM)

  • 数据报套接字(SOCK_DGRAM)

  • 原始套接字(SOCK_RAW)

前两者较常用。基于TCP的socket编程是流式套接字。

二、服务端和客户端都做了什么

服务端:

建立socket,声明自身的port和IP,并绑定到socket,使用listen监听,然后不断用accept去查看是否有连接。如果有,捕获socket,并通过recv获取消息的内容,通信完成后调用closeSocket关闭这个对应accept到的socket。如果不需要等待任何客户端连接,那么用closeSocket直接关闭自身的socket。

客户端:

建立socket,通过端口号和地址确定目标服务器,使用Connect连接到服务器,send发送消息,等待处理,通信完成后调用closeSocket关闭socket。

三次握手:

  • 第一次握手:Client将SYN(同步序列编号Synchronize Sequence Numbers)置1,随机产生一个初始序列号seq发送给Server,进入SYN_SENT状态;

  • 第二次握手:Server收到Client的SYN=1之后,知道客户端请求建立连接,将自己的SYN置1,ACK(确认字符Acknowledge Character)置1,产生一个acknowledge number=sequence number+1,并随机产生一个自己的初始序列号,发送给客户端;进入SYN_RCVD状态;

  • 第三次握手:客户端检查acknowledge number是否为序列号+1,ACK是否为1,检查正确之后将自己的ACK置为1,产生一个acknowledge number=服务器发的序列号+1,发送给服务器;进入ESTABLISHED状态;服务器检查ACK为1和acknowledge number为序列号+1之后,也进入ESTABLISHED状态;完成三次握手,连接建立。

四次挥手:

  • 第一次挥手:Client将FIN(finish)置为1,发送一个序列号seq给Server;进入FIN_WAIT_1状态;

  • 第二次挥手:Server收到FIN之后,发送一个ACK=1,acknowledge number=收到的序列号+1;进入CLOSE_WAIT状态。此时客户端已经没有要发送的数据了,但仍可以接受服务器发来的数据。

  • 第三次挥手:Server将FIN置1,发送一个序列号给Client;进入LAST_ACK状态;

  • 第四次挥手:Client收到服务器的FIN后,进入TIME_WAIT状态;接着将ACK置1,发送一个acknowledge number=序列号+1给服务器;服务器收到后,确认acknowledge number后,变为CLOSED状态,不再向客户端发送数据。客户端等待2*MSL(报文段最长寿命)时间后,也进入CLOSED状态。完成四次挥手。

Socket整体交互流程:

Windows下的代码怎么写,参考:

https://blog.csdn.net/zahngjialiang/article/details/53929584

对于函数解释的很清晰。

另外目前的教程都是有服务端和客户端的代码的,刚开始看真的很蒙,不明白要怎么运行。其实他们是把两个代码分别编译成EXE后进行测试的。其实我们可以下载一个TCP调试助手,就可以只写一端的代码进行测试了。比如下图这个:

三、Socket代码解析

1. WSAStartup函数

使用Socket之前必须调用WSAStartup函数,此函数在应用程序中用来初始化Windows Sockets DLL,只有此函数调用成功后,应用程序才可以调用Windows SocketsDLL中的其他API函数,否则后面的任何函数都将调用失败。

函数的原型:

int WSAAPI WSAStartup(
  WORD      wVersionRequested,
  LPWSADATA lpWSAData
);

函数的使用:

WSAStartup(MAKEWORD(2, 2), &wsaData);//第一个是dll版本号,第二个是创建的socket对象

2. WSADATA

WSADATA,一种数据结构。

这个结构被用来存储被WSAStartup函数调用后返回的Windows Sockets数据。

3. MAKEWORD函数

Windows Sockets DLL期望调用者使用的Windows Sockets规范的版本。 高位字节存储副版本号, 低位字节存储主版本号,可以用WORD MAKEWORD(BYTE,BYTE ) 返回这个值,例如:MAKEWORD(1,1)

前面都做完了之后,需要确认是否初始化成功:

/*
功能:
    连接网络
参数:
返回:
    0/失败、1/成功
*/
int ConnectNetwork()
{
    struct WSAData      winSock;
    int                ret;

    //初始化Windows网络环境
    ret=WSAStartup(MAKEWORD(2, 2), &winSock);
    if ( ret<0 )
    {
        printf("网络启动失败!\n");
        return 0;
    }
    return 1;
}

4. SOCKET函数

SOCKET socket(int af,int type,int protocol);
af:一个地址家族,通常为AF_INET(AF–ADDRESS FAMILY)
type:套接字类型,SOCK_STREAM表示创建面向流连接的套接字。为SOCK_DGRAM,表示创建面向无连接的数据包套接字。为SOCK_RAW,表示创建原始套接字
protocol:套接字所用协议,不指定可以设置为0 返回值就是一个socket

函数的使用:

//创建TCP socket
SOCKET serverSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (serverSocket == INVALID_SOCKET)
{
    ret = WSAGetLastError();
    return -ret;        
}

5. 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填充
};
①sin_family 和 socket() 的第一个参数的含义相同,取值也要保持一致。
sin_prot 为端口号。uint16_t 的长度为两个字节,理论上端口号的取值范围为 0~65536,但 0~1023的端口一般由系统分配给特定的服务程序,例如 Web 服务的端口号为 80,FTP 服务的端口号为 21,所以我们的程序要尽量在1024~65536 之间分配端口号。 端口号需要用 htons() 函数转换,后面会讲解为什么。
sin_addr 是 struct in_addr 结构体类型的变量,下面会详细讲解。
sin_zero[8] 是多余的8个字节,没有用,一般使用 memset() 函数填充为 0。上面的代码中,先用 memset()将结构体的全部字节填充为 0,再给前3个成员赋值,剩下的 sin_zero 自然就是 0 了。

6. memset函数

复制字符 c(一个无符号字符)到参数 str 所指向的字符串的前 n 个字符。

void *memset(void *str, int c, size_t n)

函数的使用:

struct sockaddr_in sockAddr;//创建套接字地址
memset(&sockAddr, 0, sizeof(sockAddr));//用0填充每个字节
sockAddr.sin_family = PF_INET;//使用PF_INET地址族,也就是IPv4
sockAddr.sin_addr.s_addr = inet_addr("192.168.1.200");//具体的地址
sockAddr.sin_port = htons(4196);//端口

7. in_addr 结构体(sin_addr)

sockaddr_in 的第3个成员是 in_addr 类型的结构体,该结构体只包含一个成员。in_addr_t 在头文件 <netinet/in.h> 中定义,等价于 unsigned long,长度为4个字节。也就是说,s_addr 是一个整数,而IP地址是一个字符串,所以需要 inet_addr() 函数进行转换。

struct in_addr{
  in_addr_t  s_addr;//32位的IP地址
};

8. bind()函数

sock 为 socket 文件描述符,addr 为 sockaddr 结构体变量的指针,addrlen 为 addr 变量的大小,可由 sizeof() 计算得出。

int bind(SOCKET sock, const struct sockaddr *addr, int addrlen);

函数的使用:

bind(servSock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));

9. listen()函数

sock 为需要进入监听状态的套接字,backlog 为请求队列的最大长度。

该函数只有服务端有,客户端没有

int listen(SOCKET sock, int backlog); 

函数的使用:

   //等待客户请求
    ret=listen(listenSocket, MAX_REQUESTS);
    if ( ret<0 )
    {
        closesocket(listenSocket);
        listenSocket=-1;
        printf("listen() = %u\n", WSAGetLastError());
        return -1;
    }

10. accept()函数

accept()用来接受参数s 的socket 连线。 参数s 的socket 必需先经bind()、listen()函数处理过,
当有连线进来时accept()会返回一个新的socket 处理代码, 往后的数据传送与读取就是经由新的socket处理, 而原来参数s的socket 能继续使用accept()来接受新的连线要求。 连线成功时, 参数addr 所指的结构会被系统填入远程主机的地址数据,参数addrlen 为scokaddr 的结构长度
int accept(int s, struct sockaddr * addr, int * addrlen);

函数的使用:

        //接收客户连接请求
        //clientAddr中包含客户的IP地址和端口
        *clientSocket = -1;
        *clientSocket=accept(listenSocket, (struct sockaddr *)&clientAddr, 
            (SINT32 *)&addrLen);
        if ( *clientSocket<0 ) // 错误的请求
        {
            printf("accept() = %u\n", WSAGetLastError());
            continue;
        }

11. send()函数

send()用来将数据由指定的socket 传给对方主机。 参数s 为已建立好连接的socket。 参数buf 指向欲连线的数据内容, 参数len 则为数据长度。参数flags 一般设0

int send(SOCKET s, const char *buf, int len, int flags);

12. closesocket()函数

关闭套接字,释放与套接字关联的所有资源

int closesocket(SOCKET s)

函数的使用:

closesocket(clientSocket);
closesocket(serverSocket);

13.WSACleanup()函数

终止使用WinSock,释放为应用程序分配的相关资源

四、示例代码

服务端:

#include <iostream>
#include <stdlib.h>
#include <stdio.h>
#include "winsock2.h"
 #include<cstdlib>
#pragma comment(lib,"ws2_32.lib")//引用库文件
using namespace std;


char recvBuf[100];
SOCKET sockConn;
/**
 * 在一个新的线程里面接收数据
 */
DWORD WINAPI Fun(LPVOID lpParamter)
{
             while(true){
                memset(recvBuf, 0, sizeof(recvBuf));
                //      //接收数据
                recv(sockConn, recvBuf, sizeof(recvBuf), 0);
                printf("%s\n", recvBuf);
            }
            closesocket(sockConn);
}

int main()
{
    WSADATA wsaData;
    int port = 8888;//端口号
    if(WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
    {
        printf("初始化失败");
        return 0;
    }

    //创建用于监听的套接字,即服务端的套接字
    SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);

    SOCKADDR_IN addrSrv;
    addrSrv.sin_family = AF_INET;
    addrSrv.sin_port = htons(port); //1024以上的端口号
    /**
     * INADDR_ANY就是指定地址为0.0.0.0的地址,这个地址事实上表示不确定地址,或“所有地址”、“任意地址”。
     * 一般来说,在各个系统中均定义成为0值。
     */
    addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);

    int retVal = bind(sockSrv, (LPSOCKADDR)&addrSrv, sizeof(SOCKADDR_IN));
    if(retVal == SOCKET_ERROR){
        printf("连接失败:%d\n", WSAGetLastError());
        return 0;
    }

    if(listen(sockSrv,10) ==SOCKET_ERROR){
        printf("监听失败:%d", WSAGetLastError());
        return 0;
    }

    SOCKADDR_IN addrClient;
    int len = sizeof(SOCKADDR);

    while(1)
    {
        //等待客户请求到来
        sockConn = accept(sockSrv, (SOCKADDR *) &addrClient, &len);
        if(sockConn == SOCKET_ERROR){
            printf("等待请求失败:%d", WSAGetLastError());
            break;
        }

        printf("客户端的IP是:[%s]\n", inet_ntoa(addrClient.sin_addr));

        //发送数据
        char sendbuf[] = "你好,我是服务端,咱们一起聊天吧";
        int iSend = send(sockConn, sendbuf, sizeof(sendbuf) , 0);
        if(iSend == SOCKET_ERROR){
            printf("发送失败");
            break;
        }

        HANDLE hThread = CreateThread(NULL, 0, Fun, NULL, 0, NULL);
        CloseHandle(hThread);

    }

    closesocket(sockSrv);
    WSACleanup();
    system("pause");
    return 0;
}

客户端:

#include <iostream>
#include <stdlib.h>
#include <stdio.h>
#include "winsock2.h"
#pragma comment(lib,"ws2_32.lib")//引用库文件
using namespace std;

int main()
{
    //加载套接字
    WSADATA wsaData;
    char buff[1024];
    memset(buff, 0, sizeof(buff));

    if(WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
    {
        printf("初始化Winsock失败");
        return 0 ;
    }

    SOCKADDR_IN addrSrv;
    addrSrv.sin_family = AF_INET;
    addrSrv.sin_port = htons(8888);//端口号
    addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//IP地址

    //创建套接字
    SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);
    if(SOCKET_ERROR == sockClient){
        printf("Socket() error:%d", WSAGetLastError());
        return 0;
    }

    //向服务器发出连接请求
    if(connect(sockClient, (struct  sockaddr*)&addrSrv, sizeof(addrSrv)) == INVALID_SOCKET){
        printf("连接失败:%d", WSAGetLastError());
        return 0;
    }else
    {
        //接收数据
        recv(sockClient, buff, sizeof(buff), 0);
        printf("%s\n", buff);
    }

    //发送数据
    char buffs[] = "下面咱们开始聊天了";
    send(sockClient, buffs, sizeof(buffs), 0);
    //不断输入,然后发送
    while(true){
        cin>>buffs;
        send(sockClient, buffs, sizeof(buffs), 0);
    }

    //关闭套接字
    closesocket(sockClient);
    WSACleanup();//释放初始化Ws2_32.dll所分配的资源。
    system("pause");//让屏幕暂留
    return 0;
}

本文来源:

https://blog.csdn.net/weixin_42299076/article/details/124828768

https://blog.csdn.net/zahngjialiang/article/details/53929584

  • 7
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值