一、基于TCP/IP协议的C/S模型

一、基于TCP/IP协议的C/S模型

1.1 TCP/IP协议

Transmission Control Protocol / Internet Protocol

1.1.1 TCP/IP的重要性

TCP/IP是今天的互联网的基石,没有这个就上不了网了

1.1.2 TCP/IP协议概念

tcp/ip协议族(簇,组,体系),并不是tcp协议和ip协议的总称,tcp/ip指的是整个网络传输体系。而tcp协议和ip协议就是单单的两个协议。

1.1.3 TCP/IP的特点

面向连接的,可靠的,基于字节流的传输层协议。

UDP/IP: 面向非连接的,不可靠的,基于数据报的传输层协议。

1.2 client/server ----- 客户端/服务器模型

比如QQ,DNF,LOL等这些我们下载客户端的,都属于c/s模型的一个应用

c/s模型其实是概念层面的,实现层面可以是基于任何的网络协议。

常见的还有b/s模型: 浏览器/服务器模型 基于http/https协议的

二、服务端

2.1 头文件与库文件

#include<WinSock2.h>                 // windows socket 第2版, 名字不区分大小写
#pragma comment(lib, "Ws2_32.lib")   // windows socket 第2版  32位的库文件, 名字不区分大小写

2.2 打开网络库(WSAStartup)

WORD wdVersion = MAKEWORD(2, 2);  // 将2.2版本存入 wdVersion。 类型是WORD unsigned short int
WSADATA wdSockkMsg;

/*
    int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
    wVersionRequested 为 WinSock 规范的版本号,低字节为主版本号,高字节为副版本号(修正版本号)
    lpWSAData 为指向 WSAData 结构体的指针。
*/
// 打开网络库
int nRes = WSAStartup(wdVersion, &wdSockkMsg);
if (0 != nRes)
{
    // 返回值错误码
    /*
        WSASYSNOTREADY     10091  底层网络子系统尚未准备好进行网络通信。   系统配置问题,重启下电脑,检查ws2_32库是否存在,或者是否在环境配置目录下
        WSAVERNOTSUPPORTED 10092  此特定Windows套接字实现不提供所请求的Windows套接字支持的版本。   要使用的版本不支持
        WSAEPROCLIM        10067  已达到对Windows套接字实现支持的任务数量的限制。  Windows Sockets实现可能限制同时使用它的应用程序的数量
        WSAEINPROGRESS     10036  正在阻止Windows Sockets 1.1操作。   当前函数运行期间,由于某些原因造成阻塞,会返回在这个错误码,其他操作均禁止
        WSAEFAULT          10014  lpWSAData参数不是有效指针。   参数写错了
    */
    switch (nRes)
    {
        case WSASYSNOTREADY:
            printf("检查网络库或者重启电脑试试\n");
            break;
        case WSAVERNOTSUPPORTED:
            printf("请更新网络库\n");
            break;
        case WSAEPROCLIM:
            printf("请尝试关掉不必要的软件,以为当前网络运行提供足够的资源\n");
            break;
        case WSAEINPROGRESS:
            printf("请重新启动软件\n");
            break;
        case WSAEFAULT:
            printf("函数参数错误\n");
            break;
    }
    return -1;
}

2.3 校验版本(wdSockkMsg.wVersion)

// 校验版本
if (2 != HIBYTE(wdSockkMsg.wVersion) || 2 != LOBYTE(wdSockkMsg.wVersion))
{
    // 版本不对,关闭网络库函数
    WSACleanup();
    return -1;
}

2.4 创建socket

SOCKET socketServer = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
/*
    参数1: 地址的类型
    参数2: 套接字类型
    参数3: 协议类型
*/
if (INVALID_SOCKET == socketServer)
{
    int a = WSAGetLastError();
    printf("错误码是 %d", a);
    // 清理网络库
    WSACleanup();
    return -1;
}

2.4.1 地址的类型

2.4.1.1 AF_INIT 2

ipv4: Internet协议版本4(IPv4)地址系列。
  192.168.1.103,
  0.0.0.0 ~ 255.255.255.255,
  点分十进制表示法
  4字节 32位的地址

2.4.1.2 AF_INET6 23

ipv6: Internet协议版本6(IPv6)地址系列。
  2001:0:3238:DFE1:63::FEFB
  16字节 128位的地址

2.4.1.3 AF_BTH 32

蓝牙地址系列。
如果计算机安装了蓝牙适配器和驱动程序,则Windows XP SP2或更高版本支持此地址系列。
6B:2D:BC:A9:8C:12

2.4.1.4 AF_IRDA 26

红外数据协会(IrDA)地址系列。
仅当计算机安装了红外端口和驱动程序时,才支持此地址系列。

2.4.2 套接字类型

2.4.2.1 SOCK_STREAM 1

一种套接字类型,提供带有OOB数据传输机制的顺序,可靠,双向,基于连接的字节流。 此套接字类型使用传输控制协议(TCP)作为Internet地址系列(AF_INET或AF_INET6)。

2.4.2.2 SOCK_DGRAM 2

一种支持数据报的套接字类型,它是固定(通常很小)最大长度的无连接,不可靠的缓冲区。 此套接字类型使用用户数据报协议(UDP)作为Internet地址系列(AF_INET或AF_INET6)。

2.4.2.3 SOCK_RAW 3

一种套接字类型,提供允许应用程序操作下一个上层协议头的原始套接字。 要操作IPv4标头,必须在套接字上设置IP_HDRINCL套接字选项。 要操作IPv6标头,必须在套接字上设置IPV6_HDRINCL套接字选项。

2.4.2.4 SOCK_RDM 4

一种套接字类型,提供可靠的消息数据报。 这种类型的一个示例是Windows中的实用通用多播(PGM)多播协议实现,通常称为可靠多播节目。
仅在安装了可靠多播协议时才支持此类型值。

2.4.2.5 SOCK_SEQPACKET 5

一种套接字类型,提供基于数据报的伪流数据包。

2.4.3 协议的类型

2.4.3.1 IPPROTO_TCP

传输控制协议(TCP)。 当af参数为AF_INET或AF_INET6且类型参数为SOCK_STREAM时,这是一个可能的值。

2.4.3.2 IPPROTO_UDP

用户数据报协议(UDP)。 当af参数为AF_INET或AF_INET6且类型参数为SOCK_DGRAM时,这是一个可能的值。

2.4.3.3 IPPROTO_ICMP

Internet控制消息协议(ICMP)。 当af参数为AF_UNSPEC,AF_INET或AF_INET6且类型参数为SOCK_RAW或未指定时,这是一个可能的值。

2.4.3.4 IPPROTO_IGMP

Internet组管理协议(IGMP)。 当af参数为AF_UNSPEC,AF_INET或AF_INET6且类型参数为SOCK_RAW或未指定时,这是一个可能的值。

2.4.3.5 IPPROTO_RM

用于可靠多播的PGM协议。 当af参数为AF_INET且类型参数为SOCK_RDM时,这是一个可能的值。 在针对Windows Vista及更高版本发布的Windows SDK上,此协议也称为IPPROTO_PGM。
仅在安装了可靠多播协议时才支持此协议值。

2.4.3.6 备注点

1. 通过参数3得到一个事儿,参数1 2 3三者是配套的,不是随便填的,即使用不同的协议,那要添加对应的那套参数。
2. 想要使用一个协议,咱们设备得支持才行,比如红外
3. 参数3中,有个可能这个词,所以说一般,参数3可以填写0,系统会自动帮我们选择协议类型

2.4.4 返回值

成功返回可用的socket, 不用了就一定要销毁套接字 closesocket(socketListen);
失败返回INVALID_SOCKET, 关闭网络库: WSACleanup(); 可用WSAGetLasterror()返回错误码

2.5 bind绑定地址和端口(bind)

int bind(SOCKET socketServer, const sockaddr *addr,int namelen);
SOCKADDR_IN si;  // struct sockaddr_in si;
si.sin_family = AF_INET;
si.sin_port = htons(12345);
si.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");

if (SOCKET_ERROR == bind(socketServer, (struct sockaddr *)&si, sizeof(si)))
{
    // 出错了
    int a = WSAGetLastError();
    // 释放
    closesocket(socketServer);
    WSACleanup();
    return -1;
}
/*
    参数1: socket创建的对象, 上一个函数创建了socket,绑定了协议信息(地址类型,套接字类型,协议类型),咱们bind函数就是绑定实质的地址,端口号
    参数2: 
        struct sockaddr {
            ushort  sa_family;
            char    sa_data[14];
        };

        struct sockaddr_in {
            short   sin_family;
            u_short sin_port;
            struct  in_addr sin_addr;
            char    sin_zero[8];
        };
        参数2是标志绑定的地址与端口。因为sockaddr具体怎么存储什么不清楚,而sockaddr和sockaddr
        的字节大小是一致的,所以在绑定的时候用 sockaddr_in来绑定,然后强制转换成sockaddr类型。
    参数3: 参数2的类型大小, sizeof(参数2)
    
    返回值: 成功返回0, 失败返回SOCKET_ERROR, 具体错误码通过int WSAGetLastError(void);获得 
*/

 

2.6 开始监听(listen)

int WSAAPI listen(SOCKET s, int  backlog);
if (SOCKET_ERROR == listen(socketServer, SOMAXCONN))
{
    // 出错了
    int a = WSAGetLastError();
    // 释放
    closesocket(socketServer);
    WSACleanup();
    return -1;
}
/*
    参数1: 服务器端的socket,也就是socket函数创建的
    参数2: 挂起连接队列的最大长度。我们一般填写这个参数 , SOMAXCONN
    WSAAPI 调用约定, 这个我们可以忽略,这是给系统看的,跟我们没关
    返回值: 成功返回0, 失败返回SOCKET_ERROR, 具体错误码通过int WSAGetLastError(void);获得 
*/

2.7 创建客户端socket/接受连接(accept)

SOCKET WSAAPI accept(SOCKET s,sockaddr *addr, int *addrlen);
// 创建客户端链接, 可以理解为与客户端创建三次握手的过程
struct sockaddr_in clientMsg;
int len = sizeof(clientMsg);
SOCKET socketclient = accept(socketServer, (struct sockaddr *)&clientMsg, &len);

/*
    参数1: 我们上面创建的自己的socket
    参数2: 客户端的地址端口信息结构体
    参数3: 参数2的大小
            
    参数2 3也能都设置成NULL,那就是不直接得到客户端的地址,端口号,
        此时可以通过函数得到客户端信息: getpeername(newSocket, (struct sockaddr*)&sockClient, &nLen);
        得到本地服务器信息: getsockname(sSocket, (sockaddr*)&addr, &nLen);

    返回值  成功, 返回值就是给客户端包好的socket, 与客户端通信就靠这个
            失败, 返回INVALID_SOCKET 
*/
if (INVALID_SOCKET == socketclient)
{
    printf("客户端连接失败\n");
    // 出错了
    int a = WSAGetLastError();
    // 释放
    closesocket(socketServer);
    WSACleanup();
    return -1;
}

2.8 与客户端收发消息(recv,send)

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

2.8.1 recv收消息

2.8.1.1 本质:复制

数据的接收都是由协议本身做的,也就是socket的底层做的,系统会有一段缓冲区,存储着接收到的数据。我们外边调用recv的作用,就是通过socket找到这个缓冲区,并把数据复制进咱们的参数2,复制参数3个

2.8.1.2 recv使用

// 与客户端收发消息
char buf[1500] = { 0 };
int res = recv(socketclient, buf, 1499, 0);
if (0 == res)
{
    printf("客户端链接中断\n");
    // 释放客户端链接
    closesocket(socketServer);
    return -1;
}
else if (SOCKET_ERROR == res)
{
    // 出错了
    int a = WSAGetLastError();
    printf("出错了 %d", a);
    // 根据实际情况处理
    return -1;
}
else
{
    printf("%d  %s", res, buf);
}
    

/*
    参数1: 客户端的socket,每个客户端对应唯一的socket
    参数2: 客户端消息的存储空间,也就是个字符数组, 这个一般1500字节,
        网络传输得最大单元,1500字节,也就是客户端发过来得数据,一次最大就是
        1500字节,这是协议规定,这个数值也是根据很多情况,总结出来得最优值
    参数3: 想要读取得字节个数, 一般是参数2得字节数-1,把\0字符串结尾留出来
    参数4: 数据的读取方式
        正常逻辑来说:我们从系统缓冲区把数据读到我们的buf,读到我们buf中后,
        系统缓冲区的被读的就应该被删除掉了,不然也是浪费空间,毕竟,
        通信时间长的话,那就爆炸了.我们将缓冲区的数据读到我们自己的buf,
        根据需要处理相应的数据,这是我们可控的。系统缓冲区的数据,咱们无可奈何,操作不了
        读出来的就删除的话,有很多的好处:
            1、系统缓冲区读到的数据,比我们的buf多,那么我们读出来的,系统删掉,从而我们就可以依次的把所有数据读完了
            2、可以计数收到了多少字节, 返回值就是本次读出来的数据
            3、正常这种逻辑, 填0, 读出来的就删除
        
        0,
        MSG_PEEK: 窥视传入的数据。 数据将复制到缓冲区中,但不会从输入队列中删除。读出来的不删除
            这个东西是不建议被使用的:
                1)、读数据不行
                2)、无法计数
        MSG_OOB: 带外数据, 就是传输一段数据,在外带一个额外的特殊数据
            不建议被使用了:
                1)、TCP协议规范(RFC 793)中OOB的原始描述被“主机要求”规范取代( RFC 1122),但仍有许多机器具有RFC 793 OOB实现。
                2)、既然两种数据,那咱们就send两次,另一方recv两次就行了,何必搞得那么神神秘秘,浪费计算机精力
        MSG_WAITALL: 直到系统缓冲区字节数满足参数3所请求得字节数,才开始读取
    返回值:
        读出来字节数大小,读没了咋办?在recv函数卡着,等着客户端发来数据,即阻塞,同步
        客户端下线,这端返回0,释放客户端socket
        执行失败,返回SOCKET_ERROR, WSAGetLastError()得到错误码
*/

2.8.2 send发消息

int WSAAPI send(SOCKET s, const char *buf, int len, int flags);
if (SOCKET_ERROR == send(socketclient, "abcd", sizeof("abcd"), 0))
{
    // 出错了
    int a = WSAGetLastError();
    printf("出错了 %d", a);
    return -1;

}
/*
    参数1: 目标的socket,每个客户端对应唯一的socket
    参数2: 给对方发送的字节串, 这个不要超过1500字节, 发送时候,协议要进行包装,加上协议信息,也叫协议头,或者叫包头
        这个大小不同的协议不一样,链路层14字节,ip头20字节,tcp头20字节,数据结尾还要有状态确认,加起来也几十个字节,
        所以实际咱们的数据位,不能写1500个,要留出来,那就1024吧,或者最多1400,就差不多了。
        超过1500系统咋办?系统会分片处理:
            比如2000个字节, 系统分成两个包,分两次发送出去: 1400+包头==1500(假设包头100字节),600+包头==700
            结果:
                1、系统要给咱们分包再打包,再发送, 客户端接收到了还得拆包,组合数据。从而增加了系统的工作,降低效率
                2、有的协议,就把分片后的二包直接丢了
    参数3: 要发送数据的字节个数
    参数4: 1、写0就行了
          2、 MSG_OOB: 意义同recv
          3、 MSG_DONTROUTE: 指定数据不应受路由限制。 Windows套接字服务提供程序可以选择忽略此标志。

    返回值: 成功返回写入的字节数, 执行失败,返回 SOCKET_ERROR
*/

三、客户端

3.1 头文件、库文件

#include<WinSock2.h>                 // windows socket 第2版, 名字不区分大小写
#pragma comment(lib, "Ws2_32.lib")   // windows socket 第2版  32位的库文件, 名字不区分大小写

3.2 打开网络库(WSAStartup)

WORD wdVersion = MAKEWORD(2, 2);  // 将2.2版本存入 wdVersion。 类型是WORD unsigned short int
WSADATA wdSockkMsg;

/*
    int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
    wVersionRequested 为 WinSock 规范的版本号,低字节为主版本号,高字节为副版本号(修正版本号)
    lpWSAData 为指向 WSAData 结构体的指针。
*/
// 打开网络库
int nRes = WSAStartup(wdVersion, &wdSockkMsg);
if (0 != nRes)
{
    // 返回值错误码
    /*
        WSASYSNOTREADY     10091  底层网络子系统尚未准备好进行网络通信。   系统配置问题,重启下电脑,检查ws2_32库是否存在,或者是否在环境配置目录下
        WSAVERNOTSUPPORTED 10092  此特定Windows套接字实现不提供所请求的Windows套接字支持的版本。   要使用的版本不支持
        WSAEPROCLIM        10067  已达到对Windows套接字实现支持的任务数量的限制。  Windows Sockets实现可能限制同时使用它的应用程序的数量
        WSAEINPROGRESS     10036  正在阻止Windows Sockets 1.1操作。   当前函数运行期间,由于某些原因造成阻塞,会返回在这个错误码,其他操作均禁止
        WSAEFAULT          10014  lpWSAData参数不是有效指针。   参数写错了
    */
    switch (nRes)
    {
        case WSASYSNOTREADY:
            printf("检查网络库或者重启电脑试试\n");
            break;
        case WSAVERNOTSUPPORTED:
            printf("请更新网络库\n");
            break;
        case WSAEPROCLIM:
            printf("请尝试关掉不必要的软件,以为当前网络运行提供足够的资源\n");
            break;
        case WSAEINPROGRESS:
            printf("请重新启动软件\n");
            break;
        case WSAEFAULT:
            printf("函数参数错误\n");
            break;
    }
    return -1;
}

3.3 校验版本(wdSockkMsg.wVersion)

// 校验版本
if (2 != HIBYTE(wdSockkMsg.wVersion) || 2 != LOBYTE(wdSockkMsg.wVersion))
{
    // 版本不对,关闭网络库函数
    WSACleanup();
    return -1;
}

3.4 创建socket

// 创建 socket
SOCKET socketServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (INVALID_SOCKET == socketServer)
{
    int a = WSAGetLastError();
    a = WSAGetLastError();
    printf("错误码是 %d", a);
    // 清理网络库
    WSACleanup();
    return -1;
}

3.5 链路到服务器

int WSAAPI connect(SOCKET s, const sockaddr *name, int namelen);
SOCKADDR_IN serverMsg;  // struct sockaddr_in si;
serverMsg.sin_family = AF_INET;
serverMsg.sin_port = htons(12345);
serverMsg.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
if (SOCKET_ERROR == connect(socketServer, (struct sockaddr *)&serverMsg, sizeof(serverMsg)))
{
    // 出错了
    int a = WSAGetLastError();
    printf("出错了 %d", a);
    // 释放
    closesocket(socketServer);
    WSACleanup();
    return -1;
}

3.6 与服务器收发消息

// 收发消息
char buf[1500] = { 0 };
int res = recv(socketServer, buf, 1499, 0);
if (0 == res)
{
    printf("客户端链接中断\n");
    // 释放客户端链接
    closesocket(socketServer);
    return -1;
}
else if (SOCKET_ERROR == res)
{
    // 出错了
    int a = WSAGetLastError();
    printf("出错了 %d", a);
    // 根据实际情况处理
    return -1;
}
else
{
    printf("%d  %s\n", res, buf);
}
if (SOCKET_ERROR == send(socketServer, "客户端收到了服务器的消息", sizeof("客户端收到了服务器的消息"), 0))
{
    // 出错了
    int a = WSAGetLastError();
    printf("出错了 %d", a);
    return -1;

}

四、代码整合

4.1 服务端

4.1.1 服务端(注释版)

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<string.h>
#include<WinSock2.h>                 // windows socket 第2版, 名字不区分大小写
#pragma comment(lib, "Ws2_32.lib")   // windows socket 第2版  32位的库文件, 名字不区分大小写

int main()
{
    WORD wdVersion = MAKEWORD(2, 2);  // 将2.2版本存入 wdVersion。 类型是 WORD: unsigned short int
    WSADATA wdSockkMsg;

    /*
    int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
    wVersionRequested 为 WinSock 规范的版本号,低字节为主版本号,高字节为副版本号(修正版本号)
    lpWSAData 为指向 WSAData 结构体的指针。
    */
    // 打开网络库
    int nRes = WSAStartup(wdVersion, &wdSockkMsg);
    if (0 != nRes)
    {
        // 返回值错误码
        /*
        WSASYSNOTREADY     10091  底层网络子系统尚未准备好进行网络通信。   系统配置问题,重启下电脑,检查ws2_32库是否存在,或者是否在环境配置目录下
        WSAVERNOTSUPPORTED 10092  此特定Windows套接字实现不提供所请求的Windows套接字支持的版本。   要使用的版本不支持
        WSAEPROCLIM        10067  已达到对Windows套接字实现支持的任务数量的限制。  Windows Sockets实现可能限制同时使用它的应用程序的数量
        WSAEINPROGRESS     10036  正在阻止Windows Sockets 1.1操作。   当前函数运行期间,由于某些原因造成阻塞,会返回在这个错误码,其他操作均禁止
        WSAEFAULT          10014  lpWSAData参数不是有效指针。   参数写错了
        */
        switch (nRes)
        {
        case WSASYSNOTREADY:
            printf("检查网络库或者重启电脑试试\n");
            break;
        case WSAVERNOTSUPPORTED:
            printf("请更新网络库\n");
            break;
        case WSAEPROCLIM:
            printf("请尝试关掉不必要的软件,以为当前网络运行提供足够的资源\n");
            break;
        case WSAEINPROGRESS:
            printf("请重新启动软件\n");
            break;
        case WSAEFAULT:
            printf("函数参数错误\n");
            break;
        }
    }
    // 校验版本
    if (2 != HIBYTE(wdSockkMsg.wVersion) || 2 != LOBYTE(wdSockkMsg.wVersion))
    {
        // 版本不对,关闭网络库函数
        WSACleanup();
        return -1;
    }

    // 创建socket
    SOCKET socketServer = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
    /*
    参数1: 地址的类型
    参数2: 套接字类型
    参数3: 协议类型
    */
    
    if (INVALID_SOCKET == socketServer)
    {
        int a = WSAGetLastError();
        a = WSAGetLastError();
        printf("错误码是 %d", a);
        // 清理网络库
        WSACleanup();
        return -1;
    }
    
    // bind绑定地址和端口
    SOCKADDR_IN si;  // struct sockaddr_in si;
    si.sin_family = AF_INET;
    si.sin_port = htons(12345);
    si.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
    
    if (SOCKET_ERROR == bind(socketServer, (struct sockaddr *)&si, sizeof(si)))
    {
        // 出错了
        int a = WSAGetLastError();
        printf("出错了 %d", a);
        // 释放
        closesocket(socketServer);
        WSACleanup();
        return -1;
    }
    /*
        参数1: socket创建的对象, 上一个函数创建了socket,绑定了协议信息(地址类型,套接字类型,协议类型),咱们bind函数就是绑定实质的地址,端口号
        参数2: 
            struct sockaddr {
                ushort  sa_family;
                char    sa_data[14];
            };

            struct sockaddr_in {
                short   sin_family;
                u_short sin_port;
                struct  in_addr sin_addr;
                char    sin_zero[8];
            };
            参数2是标志绑定的地址与端口。因为sockaddr具体怎么存储什么不清楚,而sockaddr和sockaddr
            的字节大小是一致的,所以在绑定的时候用 sockaddr_in来绑定,然后强制转换成sockaddr类型。
        参数3: 参数2的类型大小, sizeof(参数2)
    */
    
    // 监听
    if (SOCKET_ERROR == listen(socketServer, SOMAXCONN))
    {
        // 出错了
        int a = WSAGetLastError();
        printf("出错了 %d", a);
        // 释放
        closesocket(socketServer);
        WSACleanup();
        return -1;
    }

    // 创建客户端链接, 可以理解为创建三次握手的过程
    struct sockaddr_in clientMsg;
    int len = sizeof(clientMsg);
    SOCKET socketclient = accept(socketServer, (struct sockaddr *)&clientMsg, &len);

    /*
        参数1: 我们上面创建的自己的socket
        参数2: 客户端的地址端口信息结构体
        参数3: 参数2的大小
            
        参数2 3也能都设置成NULL,那就是不直接得到客户端的地址,端口号,
            此时可以通过函数得到客户端信息: getpeername(newSocket, (struct sockaddr*)&sockClient, &nLen);
            得到本地服务器信息: getsockname(sSocket, (sockaddr*)&addr, &nLen);

        返回值  成功, 返回值就是给客户端包好的socket, 与客户端通信就靠这个
                失败, 返回INVALID_SOCKET 
    */
    if (INVALID_SOCKET == socketclient)
    {
        printf("客户端连接失败\n");
        // 出错了
        int a = WSAGetLastError();
        printf("出错了 %d", a);
        // 释放
        closesocket(socketServer);
        WSACleanup();
        return -1;
    }
    printf("客户端链接成功\n");
    
    // 与客户端收发消息
    while (1)
    {
        char sendMsg[1500] = { 0 };
        scanf("%s", sendMsg);
        if (SOCKET_ERROR == send(socketclient, sendMsg, strlen(sendMsg), 0))
        {
            // 出错了
            int a = WSAGetLastError();
            printf("出错了 %d", a);
            return -1;

        }
        /*
            参数1: 目标的socket,每个客户端对应唯一的socket
            参数2: 给对方发送的字节串, 这个不要超过1500字节, 发送时候,协议要进行包装,加上协议信息,也叫协议头,或者叫包头
            这个大小不同的协议不一样,链路层14字节,ip头20字节,tcp头20字节,数据结尾还要有状态确认,加起来也几十个字节,
            所以实际咱们的数据位,不能写1500个,要留出来,那就1024吧,或者最多1400,就差不多了。
            超过1500系统咋办?系统会分片处理:
            比如2000个字节, 系统分成两个包,分两次发送出去: 1400+包头==1500(假设包头100字节),600+包头==700
            结果:
            1、系统要给咱们分包再打包,再发送, 客户端接收到了还得拆包,组合数据。从而增加了系统的工作,降低效率
            2、有的协议,就把分片后的二包直接丢了
            参数3: 要发送数据的字节个数
            参数4: 1、写0就行了
            2、 MSG_OOB: 意义同recv
            3、 MSG_DONTROUTE: 指定数据不应受路由限制。 Windows套接字服务提供程序可以选择忽略此标志。

            返回值: 成功返回写入的字节数, 执行失败,返回 SOCKET_ERROR
        */

        char buf[1500] = { 0 };
        int res = recv(socketclient, buf, 50, 0);
        if (0 == res)
        {
            printf("客户端链接中断\n");
            // 释放客户端链接
            closesocket(socketServer);
            return -1;
        }
        else if (SOCKET_ERROR == res)
        {
            // 出错了
            int a = WSAGetLastError();
            printf("出错了 %d", a);
            // 根据实际情况处理
            return -1;
        }
        else
        {
            printf("%d  %s", res, buf);
        }


        /*
            参数1: 客户端的socket,每个客户端对应唯一的socket
            参数2: 客户端消息的存储空间,也就是个字符数组, 这个一般1500字节,
            网络传输得最大单元,1500字节,也就是客户端发过来得数据,一次最大就是
            1500字节,这是协议规定,这个数值也是根据很多情况,总结出来得最优值
            参数3: 想要读取得字节个数, 一般是参数2得字节数-1,把\0字符串结尾留出来
            参数4: 数据的读取方式
            正常逻辑来说:我们从系统缓冲区把数据读到我们的buf,读到我们buf中后,
            系统缓冲区的被读的就应该被删除掉了,不然也是浪费空间,毕竟,
            通信时间长的话,那就爆炸了.我们将缓冲区的数据读到我们自己的buf,
            根据需要处理相应的数据,这是我们可控的。系统缓冲区的数据,咱们无可奈何,操作不了
            读出来的就删除的话,有很多的好处:
            1、系统缓冲区读到的数据,比我们的buf多,那么我们读出来的,系统删掉,从而我们就可以依次的把所有数据读完了
            2、可以计数收到了多少字节, 返回值就是本次读出来的数据
            3、正常这种逻辑, 填0, 读出来的就删除
            0,
            MSG_PEEK: 窥视传入的数据。 数据将复制到缓冲区中,但不会从输入队列中删除。读出来的不删除
            这个东西是不建议被使用的:
            1)、读数据不行
            2)、无法计数
            MSG_OOB: 带外数据, 就是传输一段数据,在外带一个额外的特殊数据
            不建议被使用了:
            1)、TCP协议规范(RFC 793)中OOB的原始描述被“主机要求”规范取代( RFC 1122),但仍有许多机器具有RFC 793 OOB实现。
            2)、既然两种数据,那咱们就send两次,另一方recv两次就行了,何必搞得那么神神秘秘,浪费计算机精力
            MSG_WAITALL: 直到系统缓冲区字节数满足参数3所请求得字节数,才开始读取
            返回值:
            读出来字节数大小,读没了咋办?在recv函数卡着,等着客户端发来数据,即阻塞,同步
            客户端下线,这端返回0,释放客户端socket
            执行失败,返回SOCKET_ERROR, WSAGetLastError()得到错误码
        */
    }
    
    // 关闭socket
    closesocket(socketclient);
    closesocket(socketServer);
    // 清理网络库
    WSACleanup();
    return EXIT_SUCCESS;
}

4.1.2 服务端(无注释版)

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<string.h>
#include<WinSock2.h>                 // windows socket 第2版, 名字不区分大小写
#pragma comment(lib, "Ws2_32.lib")   // windows socket 第2版  32位的库文件, 名字不区分大小写

int main()
{
    WORD wdVersion = MAKEWORD(2, 2);  
    WSADATA wdSockkMsg;

    // 打开网络库
    int nRes = WSAStartup(wdVersion, &wdSockkMsg);
    if (0 != nRes)
    {
        switch (nRes)
        {
        case WSASYSNOTREADY:
            printf("检查网络库或者重启电脑试试\n");
            break;
        case WSAVERNOTSUPPORTED:
            printf("请更新网络库\n");
            break;
        case WSAEPROCLIM:
            printf("请尝试关掉不必要的软件,以为当前网络运行提供足够的资源\n");
            break;
        case WSAEINPROGRESS:
            printf("请重新启动软件\n");
            break;
        case WSAEFAULT:
            printf("函数参数错误\n");
            break;
        }
    }
    
    // 校验版本
    if (2 != HIBYTE(wdSockkMsg.wVersion) || 2 != LOBYTE(wdSockkMsg.wVersion))
    {
        WSACleanup();
        return -1;
    }

    // 创建socket
    SOCKET socketServer = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
    if (INVALID_SOCKET == socketServer)
    {
        int a = WSAGetLastError();
        a = WSAGetLastError();
        printf("错误码是 %d", a);
        WSACleanup();
        return -1;
    }
    
    // bind绑定地址和端口
    SOCKADDR_IN si;  // struct sockaddr_in si;
    si.sin_family = AF_INET;
    si.sin_port = htons(12345);
    si.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
    
    if (SOCKET_ERROR == bind(socketServer, (struct sockaddr *)&si, sizeof(si)))
    {
        // 出错了
        int a = WSAGetLastError();
        printf("出错了 %d", a);
        // 释放
        closesocket(socketServer);
        WSACleanup();
        return -1;
    }
    
    // 监听
    if (SOCKET_ERROR == listen(socketServer, SOMAXCONN))
    {
        int a = WSAGetLastError();
        printf("出错了 %d", a);
        closesocket(socketServer);
        WSACleanup();
        return -1;
    }

    // 创建客户端链接, 可以理解为创建三次握手的过程
    struct sockaddr_in clientMsg;
    int len = sizeof(clientMsg);
    SOCKET socketclient = accept(socketServer, (struct sockaddr *)&clientMsg, &len);

    if (INVALID_SOCKET == socketclient)
    {
        printf("客户端连接失败\n");
        // 出错了
        int a = WSAGetLastError();
        printf("出错了 %d", a);
        // 释放
        closesocket(socketServer);
        WSACleanup();
        return -1;
    }
    printf("客户端链接成功\n");
    
    // 与客户端收发消息
    while (1)
    {
        char sendMsg[1500] = { 0 };
        scanf("%s", sendMsg);
        if (SOCKET_ERROR == send(socketclient, sendMsg, strlen(sendMsg), 0))
        {
            int a = WSAGetLastError();
            printf("出错了 %d", a);
            return -1;
        }

        char buf[1500] = { 0 };
        int res = recv(socketclient, buf, 50, 0);
        if (0 == res)
        {
            printf("客户端链接中断\n");
            closesocket(socketServer);
            return -1;
        }
        else if (SOCKET_ERROR == res)
        {
            int a = WSAGetLastError();
            printf("出错了 %d", a);
            return -1;
        }
        else
        {
            printf("%d  %s", res, buf);
        }
    }
    
    // 关闭socket
    closesocket(socketclient);
    closesocket(socketServer);
    // 清理网络库
    WSACleanup();
    return EXIT_SUCCESS;
}

4.2 客户端

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<WinSock2.h>
#include<string.h>
#pragma comment(lib, "Ws2_32.lib")

int main()
{
    WORD wdVersion = MAKEWORD(2, 2);  // 将2.2版本存入 wdVersion。 类型是 WORD: unsigned short int
    WSADATA wdSockkMsg;

    // 打开网络库
    int nRes = WSAStartup(wdVersion, &wdSockkMsg);
    if (0 != nRes)
    {
        // 返回值错误码
        switch (nRes)
        {
        case WSASYSNOTREADY:
            printf("检查网络库或者重启电脑试试\n");
            break;
        case WSAVERNOTSUPPORTED:
            printf("请更新网络库\n");
            break;
        case WSAEPROCLIM:
            printf("请尝试关掉不必要的软件,以为当前网络运行提供足够的资源\n");
            break;
        case WSAEINPROGRESS:
            printf("请重新启动软件\n");
            break;
        case WSAEFAULT:
            printf("函数参数错误\n");
            break;
        }
        return -1;
    }

    // 校验版本
    if (2 != HIBYTE(wdSockkMsg.wVersion) || 2 != LOBYTE(wdSockkMsg.wVersion))
    {
        // 版本不对,关闭网络库函数
        WSACleanup();
        return -1;
    }

    // 创建 socket
    SOCKET socketServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (INVALID_SOCKET == socketServer)
    {
        int a = WSAGetLastError();
        a = WSAGetLastError();
        printf("出错了 %d", a);
        WSACleanup();
        return -1;
    }

    // 连接服务器
    SOCKADDR_IN serverMsg;  // struct sockaddr_in si;
    serverMsg.sin_family = AF_INET;
    serverMsg.sin_port = htons(12345);
    serverMsg.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
    if (SOCKET_ERROR == connect(socketServer, (struct sockaddr *)&serverMsg, sizeof(serverMsg)))
    {
        int a = WSAGetLastError();
        printf("出错了 %d", a);
        closesocket(socketServer);
        WSACleanup();
        return -1;
    }

    // 循环收发消息
    while (1)
    {
        char buf[1500] = { 0 };
        int res = recv(socketServer, buf, 50, 0);
        if (0 == res)
        {
            printf("客户端链接中断\n");
            closesocket(socketServer);
            return -1;
        }
        else if (SOCKET_ERROR == res)
        {
            int a = WSAGetLastError();
            printf("出错了 %d", a);
            return -1;
        }
        else
        {
            printf("%d  %s\n", res, buf);
        }
        scanf("%s", buf);
        if (SOCKET_ERROR == send(socketServer, buf, strlen(buf), 0))
        {
            int a = WSAGetLastError();
            printf("出错了 %d", a);
            return -1;
        }
    }
    

    // 关闭socket
    closesocket(socketServer);
    // 清理网络库
    WSACleanup();
    return 0;
}

 

posted on 2022-02-24 11:22  软饭攻城狮  阅读(192)  评论(0编辑  收藏  举报

导航