乐趣区

网络编程CS模型总结

什么是 socket

将底层复杂的协议体系,执行流程,进行了封装,封装完的结果,就是一个 SOCKET,也就是说,SOCKET 是我们调用协议进行通信的操作 接口

数据类型:SOCKET 转定义:unsigned int

在系统里每一个 socket 对应着 == 唯一的一个整数 ==,比如 23,对应着 socket 的协议等信息,在通信中,就使用这些整数进行通信,系统会自动去找这些整数所对应的协议

应用

每个客户端有一个 socket,服务器有一个 socket,通信时就是通过 socket,来表示和谁传递信息


创建 socket

/* 函数原型 */
SOCKET socket(
  int af,       /* 地址的类型 */
  int type,     /* 套接字类型 */
  int protocol  /* 协议类型 */
);
参数 1:地址类型
地址类型 形式
==AF_INET== 192.168.1.103(IPV4,4 字节,32 位地址)
AF_INET6 2001:0:3238:DFE1:63::FEFB(IPV6,16 字节,128 位地址)
AF_BTH 6B:2D:BC:A9:8C:12(蓝牙)
AF_IRDA 红外

通信地址不止只有 IP 地址

参数 2:套接字类型
类型 用处
==SOCK_STREAM== 提供带有 OOB 数据传输机制的顺序,可靠,双向,基于连接的字节流。使用传输控制协议(TCP)作为 Internet 地址系列(AF_INET 或 AF_INET6)
SOCK_DGRAM 支持数据报的套接字类型,它是固定(通常很小)最大长度的无连接,不可靠的缓冲区。使用用户数据报协议(UDP)作为 Internet 地址系列(AF_INET 或 AF_INET6)
SOCK_RAW 提供允许应用程序操作下一个上层协议头的原始套接字。要操作 IPv4 标头,必须在套接字上设置 IP_HDRINCL 套接字选项。要操作 IPv6 标头,必须在套接字上设置 IPV6_HDRINCL 套接字选项
SOCK_RDW 提供可靠的消息数据报。这种类型的一个示例是 Windows 中的实用通用多播(PGM)多播协议实现,通常称为可靠多播节目
SOCK_SEQPACKET 提供基于数据报的伪流数据包
参数 3:协议类型
协议类型 用处
IPPROTO_TCP 传输控制协议(TCP)
IPPROTO_UDP 用户数据报协议(UDP)
IPPROTO_ICMP Internet 控制消息协议(ICMP)
IPPROTO_IGMP Internet 组管理协议(IGMP)
IPPROTO_RM 用于可靠多播的 PGM 协议

== 填写 0 == 代表系统自动帮我们选择协议类型

==参数 1、2、3 是要相互配套使用的==,不能随便填,使用不同的协议就要添加不同的参数

返回值
  • 成功返回可用的socket 变量
  • 失败返回 INVALID_SOCKET,可以使用WSAGetlasterror() 返回错误码
if (INVALID_SOCKET == socketServer)
{int a = WSAGetLastError();
    WSACleanup();
    return 0;
}

创建 socket 代码

SOCKET socketListen = socket(AF_INET,SOCK_STREAM,0);
if(INVALID_SOCKET == socketListen)
{int a = WSAGetLastError();
    WSACleanup();
    return 0;
}

bind()函数

作用:给 socket 绑定端口号与地址

  • 地址:IP 地址
  • 端口号

    • 同一个软件可能占用多个端口号(不同功能)
    • 每一种通信的端口号是唯一的
int bind
(
  SOCKET s,    /* 服务器创建的 socket*/
  const sockaddr *addr,  /* 绑定的端口和具体地址 */
  int namelen  /*sizeof(sockaddr)*/
);
参数 1:== 被绑定 socket 变量 ==
参数 2:绑定端口号和地址

定义一个 ==SOCKADDR_IN== 数据类型,是一个结构体:

typedef struct sockaddr_in {#if(_WIN32_WINNT < 0x0600)
    short   sin_family; /* 地址类型 */
#else //(_WIN32_WINNT < 0x0600)
    ADDRESS_FAMILY sin_family;
#endif //(_WIN32_WINNT < 0x0600)

    USHORT sin_port;   /* 端口号 */
    IN_ADDR sin_addr;  /* IP 地址 */
    CHAR sin_zero[8];
} SOCKADDR_IN, *PSOCKADDR_IN;

其中 IN_ADDR sin_addr; 又是一个结构体

typedef struct in_addr {
        union {struct { UCHAR s_b1,s_b2,s_b3,s_b4;} S_un_b;
                struct {USHORT s_w1,s_w2;} S_un_w;
                ULONG S_addr;
        } S_un;
#define s_addr  S_un.S_addr /* can be used for most tcp & ip code */
#define s_host  S_un.S_un_b.s_b2    // host on imp
#define s_net   S_un.S_un_b.s_b1    // network
#define s_imp   S_un.S_un_w.s_w2    // imp
#define s_impno S_un.S_un_b.s_b4    // imp #
#define s_lh    S_un.S_un_b.s_b3    // logical host
} IN_ADDR, *PIN_ADDR, FAR *LPIN_ADDR;

127.0.0.1 本地回环地址,用于本地网络测试,数据不出计算机

端口号:0~65535

  • 0~1013:系统保留占用端口号

    • 21 端口分配给 FTP(文件传输协议)服务
    • 25 端口分配给 SMTP(简单邮件传输协议)服务
    • 80 端口分配给 HTTP 服务
  • 1024~5000:很多系统把这个取余分配给客户端
  • ==5000~65535==:我们选用的最佳范围
  • 49151~65535:系统动态随机端口
查看端口号使用情况
  • 打开运行 cmd 输入 netstat -ano -> 查看被使用的所有端口
  • netstat -ano|findstr“端口号”-> 检查端口号是否被使用了

si.sin_port = ==htons(12345)==;
si.sin_addr.S_un.S_addr = ==inet_addr(“127.0.0.1”)==;
si.sin_family = ==AF_INET==;

参数 3:参数 2 类型大小

sizeof(sockadd)

返回值
  • 成功返回0
  • 失败返回SCOKET_ERROR
if (SOCKET_ERROR == bind(socketListen,(struct sockaddr*)&sockAddress,sizeof(sockAddress)))
{printf("bind fail!");
    //int nError = ::WSAGetLastError();
    // 关闭库
    closesocket(socketListen);
    WSACleanup();
    return -1;
}

绑定端口与地址代码

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");  /*IP 地址,点分十进制 */

if (SOCKET_ERROR == bind(socketListen,(struct sockaddr*)&sockAddress,sizeof(sockAddress)))
{printf("bind fail!");
    //int nError = ::WSAGetLastError();
    // 关闭库
    closesocket(socketListen);
    WSACleanup();
    return -1;
}

listen()函数

作用:将 socket 置于侦听传入连接的状态(服务器可以接受客户端链接了)

int WSAAPI listen
(
  SOCKET s,  /* 服务器端的 socket*/
  int backlog  /* 挂起连接队列的最大长度 */
);
参数 1:== 接受链接的 socket==
参数 2:挂起连接队列的最大长度

比如有 100 个用户链接请求,但是系统一次只能处理 20 个,那么剩下的 80 个不能不理人家,所以系统就创建个队列记录这些暂时不能处理,过一会儿处理的链接请求,依先后顺序处理,那这个队列到底多大?就是这个参数设置,比如 2,那么就允许两个新链接排队的。这个肯定不能无限大,那内存不够了。

  • 填写 ==SOMAXCONN==,让系统自动选择最合适的个数
返回值
  • 成功返回0
  • 失败返回SOCKET_ERROR
if (SOCKET_ERROR == listen(socketServer, SOMAXCONN))
{int a = WSAGetLastError();
    WSACleanup();
    return 0;
 }

开启监听代码

if (SOCKET_ERROR == listen(socketListen,2))
{printf("listen fail!");
    // 关闭库
    closesocket(socketListen);
    WSACleanup();
    return -1;
}

accept()

在服务器端上创建一个新的 socket,将客户端的信息和新的 socket 绑定在一个,一次只能创建一个

SOCKET WSAAPI accept
(
  SOCKET s,   /* 服务器的 socket*/
  sockaddr *addr,  /* 返回客户端地址端口信息结构体 */
  int *addrlen   /* 返回参数 2 的类型大小 */
);
参数 1:== 服务器的 socket==
  • 为什么是服务器的 socket,不是客户端的 socket?

理解:通过服务器端的 socket,读取客户端的信息

参数 2:客户端端口地址信息结构体
  • bind() 的第二个参数 一样(地址类型、端口号、IP 地址)
  • 系统会帮我们自动填写,所以我们可以设置为 ==NULL==
/* 直接通过函数得到客户端的端口号、IP 地址 */
getpeername(newSocket,(struct sockaddr *)&sockClient,&nLen);
/* 得到本地服务器信息 */
getsockname(sSocket,(struct sockaddr *)&addr,&nLen);
参数 3:返回参数 2 的数据类型大小

如果参数 2、3 都填 ==NULL==,那么就是不直接得到客户端的地址和端口号

返回值
  • 成功返回 建立好的客户端 socket
  • 失败返回INVALID_SOCKET
if (INVALID_SOCKET == socketClient)
{int a = WSAGetLastError();
    closesocket(socketServer);
    WSACleanup();}

accept() 返回一个新的套接字来和客户端通信,addr 保存了客户端的 IP 地址和端口号,而 s 是服务器端的套接字,注意区分。接下来和客户端通信时,要使用这个新生成的套接字,而不是原来服务器端的套接字。

SOCKET newSocket;
newSocket = accept(socketListen, NULL, NULL);
if (INVALID_SOCKET == newSocket)
{printf("listen fail!");
    // 关闭库
    closesocket(socketServer);
    WSACleanup();}

accept 调试

  • == 阻塞 ==、同步

没有客户端链接就一直卡着

  • == 一次只能链接一个 ==

recv()

  • 作用:得到指定客户端发来的消息
  • 原理:复制

数据的接收都是协议本身在做,也就是 socket 底层在操作,系统有一段缓冲区,储存着接收到的数据。

recv 的作用,就是通过 socket 找到了这个缓冲区,把数据复制放到自己的数组中

int recv
(
  SOCKET s,   /* 客户端的 socket,每个客户端对应唯一的 socket*/
  char *buf,  /* 数据缓冲区 */
  int len,    /* 数据长度 */
  int flags   /* 读取方式 */
);
参数 1:== 接收端的 socket==
参数 2:客户端消息的存储空间,== 字符数组 ==,一般不大于 1500 字节
  • 为什么不大于 1500 字节?

解释:因为网络传输的最大单元是 1500 字节,这是协议规定的

参数 3:存储空间的大小(字节)

一般是 == 参数 2 的字节 -1==,把“0”字符串结尾保留下来

参数 4:数据读取方式
读取方式 作用
==0== 从系统缓冲区读到 buf 缓冲区,将系统缓冲区的数据删掉,读出来就删
MSG_PEEK 数据复制到 buf 缓冲区,但是数据 不从系统缓冲区删除
MSG_OOB 传输一段数据,再外带 加一个额外的特殊数据
MSG_WAITTALL 直到系统缓冲区 字节数满足参数 3 的数目,才开始读取
返回值
  • 成功返回 == 读出来的字节大小 ==
  • 客户端下线,返回 ==0==
  • 失败返回 ==SOCKET_ERROR==

接受数据代码

char szRecvBuffer[1500] = {0};  /* 字符数组 */
int nReturnValue = recv(newSocket, szRecvBuffer, sizeof(szRecvBuffer)-1, 0);
if (0 == nReturnValue)
{
    // 客户端正常关闭   服务端释放 Socket
    continue ;
}
else if (SOCKET_ERROR == nReturnValue)
{
    // 网络中断  
    printf("客户端中断连接");
    continue;    
}
else
{
    // 接收到客户端消息 
    printf("Client Data : %s \n",szRecvBuffer);
}

send()

  • 作用:向客户端发送数据
  • 原理:将我们的数据粘贴进系统系统缓冲区,交给系统伺机发送出去
int WSAAPI send
(
  SOCKET s,  /* 客户端 socket*/
  const char *buf,  /* 发送的字符数组 */
  int len,  /* 发送长度 */
  int flags  /* 发送方式 */
);
参数 1:== 发送端的 socket==
参数 2:给客户端发送的 == 字符数组 ==,不大于 1500 字节
  • 不要超过 1500 字节

发送的时候,协议要进行包装,加上协议信息(包头),链路层 14 字节,ip 头 20 字节,tcp 头 20 字节,数据结尾还要有状态确认,加起来也几十个字节,所以不能写 1500 个字节,最多 1400 字节(或者 1024)。

  • 如果超过 1500 字节

系统会 分片处理,分两个包,假设 2000 字节的包,1400+ 包头 =1500,600+ 包头 =700。那么系统就要分包 -> 打包 -> 发送,客户端接收到要拆包 -> 组合数据。

参数 3:== 发送字节数 ==
参数 4:发送方式
发送方式 作用
==0== 默认
MSG_OOB 传输一段数据,再外带加一个额外的特殊数据
MSG_DONTROUTE 数据不应受路由限制
返回值
  • 成功返回 发送的字节数
  • 失败返回 SOCKET_ERROR(客户端 正常下线 也是返回这个)
if (SOCKET_ERROR == send(socketClient, "abcd", sizeof("abcd"), 0))
{
    // 出错了
    int a = WSAGetLastError();
    // 根据实际情况处理
}

发送数据代码

char szSendBuffer[1024]; /* 发送字符数组 */
send(newSocket, "repeat over", strlen(szSendBuffer)+1, 0);

connect()

作用:客户端链接服务器端,将本机的一个指定的 socket 连接到一个指定地址的服务器 socket 上去

理解:connect 将在本机和指定服务器间建立一个连接。但实际上,connect 操作并不引发网络设备传送任何的数据到对端。它所 做的操作只是通过路由规则和路由表等一些信息,在 struct socket 结构中填入一些有关对端服务器的信息。这样,以后向对端发送数据报时,就不需要每次进行路由查询等操作以确定对端地址信息和本地发送接口,应用程序也就不需要每次传入对端地址信息

int WSAAPI connect
(
  SOCKET s,  /* 客户端创建的链接服务器的 socket*/
  const sockaddr *name, /* 服务器 IP 地址端口号结构体 */
  int namelen  /*sizeof(sockaddr)*/
);
参数 1:客户端创建的用来链接服务器的 socket
参数 2:服务器的 IP 地址和端口号
参数 3:参数 2 的结构体大小
返回值
  • 成功返回 发送的字节数
  • 失败返回SOCKET_ERROR
if (SOCKET_ERROR == connect(socketServer, (struct sockaddr *)&serverMsg, sizeof(serverMsg)))
{int a = WSAGetLastError();
    closesocket(socketServer);
    WSACleanup();}

客户端链接服务器代码

struct sockaddr_in serverMsg;
serverMsg.sin_family = AF_INET;  /* 地址类型 */
serverMsg.sin_port = htons(12345); /* 服务器端口 */
serverMsg.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); /* 服务器 IP*/
connect(socketServer, (struct sockaddr *)&serverMsg, sizeof(serverMsg));
if (SOCKET_ERROR == connect(socketServer, (struct sockaddr *)&serverMsg, sizeof(serverMsg)))
{int a = WSAGetLastError();
    closesocket(socketServer);
    WSACleanup();}


CS 模型存在的问题

accept()、recv()阻塞问题

  • 由于 accept()recv() 是阻塞的,做其中一件事,另外一件事就做不了。

    • 如果我们在等着收消息 recv,来了一个链接请求,那就无法处理。
    • 如果等的 socket 没有发送请求,也是一直等。

解决办法

  • 我们可以 主动和系统要有请求的 socket

    • 得到链接请求,处理 accept 函数
    • 得到发来的消息,处理 recv 函数

解决问题

  • 为什么服务器 socket 有响应的时候就是 accept?

答:因为服务器接收、发送数据都是通过绑定客户端信息的 socket 进行的,不是通过服务器 socket,服务器 socket 只是接受客户端请求的链接,并且把客户端的信息绑定到一个新的 socket 上,以后的通信都是通过这个 socket,所以服务器有响应就是有新的请求链接

  • 为什么客户端 socket 从头到尾都是用的同一个 socket?

答:客户端所创建的 socket 只是本机和指定服务器间建立一个连接,socket 结构中填入一些有关对端服务器的信息。这样,以后向对端发送数据报时,就不需要每次进行路由查询等操作以确定对端地址信息和本地发送接口,可以理解为客户端的所创建的 socket 其实就是和服务器数据交换的 socket,与服务器端最开始创建的 socket 不同。

退出移动版