网络编程——CS模型(总结)

0

什么是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不同。

你可能感兴趣的

载入中...