套接字

其实质是 内核借助缓冲区临时形成的一个可的文件描述符,是对底层TCP、UDP数据传输的封装。
数据传输的地址指定时期:

  • 服务端bind,绑定己方的IP地址信息,就是给数据传输指定己方地址信息。
  • UDP客户端connect,就是客户端的数据传输指定目标地址信息。
  • TCP客户端connect,就是客户端与服务端的数据传输之前的握手操作。记录目标地址信息。
  • 套接字通信必须是成对出现的。
  • 一个套接字管理的是一方的IP地址和端口等信息。借助套接字可以操作文件那样操作读与写缓冲区(输入、输出缓冲区)。

UDP与TCPconnect区别

  1. connect不是TCP专属,UDP中也可以使用。

  2. UDP中connect操作与TCP中connect操作有着本质区别。

  3. TCP中调用connect会引起三次握手,client与server建立连结.UDP中调用,不会TCP三次握手,仅仅限制唯一的通信目标,connect内核仅仅把对端ip&port记录下来.用于以后read或者write而不需要特定指定对方IP地址。

  4. UDP中可以多次调用connect,且同同一个服务器地址通信不需要重复指定IP地址,TCP只能调用一次connect.

  5. UDP多次调用connect有两种用途:

    • 指定一个新的ip&port连结.
    • 断开和之前的ip&port的连结.
    struct socketaddr_in{
    sa_family_t san_family; /* 地址族 */
    unit16_t sin_port; /* 端口 */
    struct in_addr_sin_addr; /* ip地址 */
    unsigned char sin_zero[8]; /* 占位字节 */

    }

    指定新连结,直接设置connect第二个参数即可.
    断开连结,需要将connect第二个参数中的sin_family设置成 AF_UNSPEC即可.

  6. UDP中使用connect可以提高效率.原因如下:

    普通的UDP发送两个报文内核做了如下:

    1. 根据发送目标的地址信息突发式向目标发送报文(sendto,recvfrom)
    2. 第二次根据发送目标的地址信息突发式发送报文

    临时指定的IP地址信息,每次发送报文内核都由可能要做路由查询.

    采用connect方式的UDP发送两个报文内核如下处理:

    1. 标明目标地址信息,对通信目标进行限制
    2. (write,read)发送,接收报文
    3. 第二次发送不需要额外指定信息,直接通信。
    4. 需要向其他的目标通信。即通过 connect指定其他的通信目标信息。
    • 另外connect的UDP不会发起任何分组交换,不会检查远程端点的合法性。
  7. 采用connect的UDP发送接受报文可以调用send,write和recv,read操作.当然也可以调用sendto,recvfrom.

    #include<sys/type.h>
    #include<sys/socket.h>
    int sendto(int sockfd,const void * buf,int len,int falgs,const struct sockaddr *to,int tolen);

    int recvfrom(int sockfd,void * buf,int len,int flags,struct sockaddr * from ,int * fromlen);

    调用sendto的时候第五个参数必须是NULL,第六个参数是0。调用recvfrom,recv,read系统调用只能获取到先前connect的ip&port发送的报文.

  8. UDP中使用connect的好处:

  9. 会提升效率.前面已经描述了.

  10. 高并发服务中会增加系统稳定性.

    原因:假设client A 通过非connect的UDP与server B,C通信.B,C提供相同服务.为了负载均衡,我们让A与B,C交替通信.

    • A 与 B通信IPa:PORTa <----> IPb:PORTb;
    • A 与 C通信IPa:PORTa’ <---->IPc:PORTc

    假设PORTa 与 PORTa’相同了(在大并发情况下会发生这种情况),那么就有可能出现A等待B的报文,却收到了C的报文.导致收报错误.解决方法内就是采用connect的UDP通信方式.在A中创建两个udp,然后分别connect到B,C.

参考文章:

UDP中使用connect的作用


套接字地址的各种具体类型

ipv

struct sockaddr_in {
sa_family_t sin_family; /* AF_INET ipv */
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. 32位地址 */

};
typedef uint32_t in_addr_t;
struct in_addr {
in_addr_t s_addr; /* IPv4 address */
};

ipv5

struct sockaddr_in6 {
sa_family_t sin6_family; /* AF_INET6 */
in_port_t sin6_port; /* Transport layer port # */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address 128位的IP地址 */
uint32_t sin6_scope_id; /* IPv6 scope-id */
};


struct in6_addr {
union {
uint8_t u6_addr8[16];
uint16_t u6_addr16[8];
uint32_t u6_addr32[4];
} in6_u;

};

通用类型1

struct sockaddr { 
sa_family_t sa_family; /* Address family */
char sa_data[14]; /* protocol-specific address */
};

通用类型2

struct sockaddr_storage{
sa_family_t ss_family; /* Address family, etc. */
char __ss_padding[_SS_PADSIZE];
__ss_aligntype __ss_align; /* Force desired alignment. */
};

sockaddr_storage能够容纳系统支持的任何套接字地址结构。

  • sock_addr实际中并不使用该结构体,只是在传参时的作为通用类型转换。
  • socket API可以接受各种类型的sockaddr结构体指针做参数,例如bind、accept、connect等函数,这些函数的参数应该设计成void *类型以便接受各种类型的指针,但是sock API的实现早于ANSI C标准化,那时还没有void *类型,因此这些函数的参数都用struct sockaddr *类型表示,在传递参数之前要强制类型转换一下,在实际使用中我们也要将其转换为具体的结构体类型,才能对其中的数据访问。

字节序

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存,因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。也即,在网络字节序看来,先发出的是数据的高位,后发出的是数据的地位。
  • 通常使用的个人电脑是小端字节序,也就是数据的低位存储在地址的地位,高位存储在地址的高位,显然与网络字节序违背,所以在write或者sendto将内容copy到输出缓冲区(发送端)前,要将内容转换为网络字节序。相应在由输入缓冲区(结束端)read或者recvfrom后要将网络字节序的数据转换为主机字节序。
  • 对应的字节序转换函数,htonl(htons)与ntohl(htons)的使用中,主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机已经是大端字节序,这些函数不做转换,将参数原封不动地返回。

bind的参数

第三个参数作用:

第二个参数 struct sockaddr *是一个通用指针类型,addr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度。

accept的第二三个参数同原理。第二个保存客户端的IP地址信息,三个参数记录第二个参数结构体的大小。

第二个参数 INADDR_ANY作用

网络地址为INADDR_ANY,这个宏(0)表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址,

当绑定到某个具体IP地址后,表示将限制仅仅监听某个IP地址,且套接字只能接受发送到此 IP 地址的连接请求。端口同

accept

第二个参数为 sock_addr结构体类型的指针,第三个参数是socklen_t 类型的指针,时输入输出类型参数。第二个参数为null表示不关心客户端的地址。

revfrom

recvform(int sock, void *buf,size_t len,int flags,struct sockaddr * from ,socklen_t * fromlen);

recvform在服务端使用时,且是是基于非连接的数据传输服务时,必须一次读出输入缓冲区中的内容,不管下次客户端是否还发来数据,上一次发送的数据没读完,就会被清除。
同样,如果输入缓冲区中的内容太大,无法将其全部读出,多余的字节也会被丢掉。

sock:索引将要从其接受数据的套接字
buf: 存放消息接收后的缓冲区
len: buf指向的缓冲区容量
falgs: 标志类型 指定该操作是否阻塞
from: 发送发地址 null时不存储改地址
fromlen:作为入口参数,存储from缓冲区的最大容量,作为出口参数,记录from缓冲区实际的使用内存大小。

阻塞函数

connect(等待TCP连接三次握手成功)
accept(等待返回建立的连接)
read(等待输入缓冲区有数据可读)
recvfrom

错误函数处理

#include<string.h>
char* strerror(int errornum);返回错误码对应的错误原因字符串
int errornum:错误码

int errexit(const char *format,...)

#include<stdarg.h>
va_list:该变量主要用来操纵整个可变参数列表
void va_start(va_list ap,argN); 初始化va_list类型的参数ap,并且使得ap指向第一个可选参数
argN:紧邻可变参数的前面一个固定参数

void va_copy(va_list dest,va_list src);用于复制va_list类型的变量

type var_arg(va_list ap,type);返回参数ap所指向的列表中的参数的下一个参数,每一次调用都会修改ap的值,这样就能正确的返回参数列表中的所有参数值
type:存储参数ap所指向的参数的数据类型

void var_end(va_list ap);每次调用va_start()函数和v_copy()函数之后,都要调用va_end()函数来销毁变量ap,即将指针置为NULL

//以下代码保存于文件errexit.c
#include <stdarg.h>
/*提供C标准库的 va_list 类型和 va_start 、 va_arg 、 va_end 宏的定义*/
#include <stdio.h>
/*标准输入输出头文件,包含了标准输入输出函数(如perror和printf等)的定义*/
#include <stdlib.h>
/*C标准库头文件,包含了C语言标准库函数(如exit、atoi等)原型的定义*/


int errexit(const char *format,...)
{
/*定义可变参数函数errexit(),该函数包含一个固定参数format,可变参数用省略号"…"表示*/
va_list args; //声明一个va_list类型的变量args
va_start(args, format);
/*对变量args进行初始化,使得args指向可变参数列表中的第一个可选参数;format为紧邻可变参数"…"的前面一个固定参数*/
vfprintf(stderr, format, args);
/*调用vfprintf()函数向标准输出错误文件输出一个出错信息*/
va_end(args);
//调用va_end()函数来销毁变量args,释放其所占资源
exit(1);
}
/* 此处 args类似于迭代器 ,初始化是va_start开始,关闭时va_end */

参考文章

监听套接字与连接套接字区别

监听套接字与连接套接字区别

  • 监听套接字只是处理连接请求,不处理连接后的数据发送。实际上收到连接请求,会创建专门的连接套接字用于之后的数据发送。

  • 监听套接字关闭(close)后,对由其参与创建的连接套接字的使用无影响。

    • 原因 :描述符引用计数

    在并发服务器中,父进程关闭已连接套接字,只是导致相应描述符的引用计数减1。既然引用计数值大于0,这个close调用并不引起TCP的四组连接终止序列。对于父进程与子进程共享已连接套接字的并发服务器来说,正是所期望的。

    • 如果我们确实想在某个TCP连接上发送一个FIN,可以改用shutdown代替close。

    如果父进程对每个由accept返回的已连接套接字都不调用close,那么并发服务器中将会发生什么?

    • 首先,父进程最终将耗尽可用描述符,因为任何进程在任何时刻可拥有着的打开的描述符通常是有限制的。不过重要的是,没有一个客户连接会被终止。当子进程关闭已连接套接字时,它的引用计数值将由2减为1,且保持为1,因为父进程永远不关闭任何已连接套接字。这将妨碍TCP连接终止序列的发生,导致连接一直打开着。

并发流式套接字服务器编程