unix socket 编程总结

socket type

SOCK_STREAM

SOCK_STREAM 是一种可靠的、双向的通信数据流,数据可以准确无误地到达另一台计算机,如果损坏或丢失,可以重新发送。因为它使用了 TCP 协议.流格式套接字的内部有一个缓冲区(也就是字符数组),通过 socket 传输的数据将保存到这个缓冲区。接收端在收到数据后并不一定立即读取,只要数据不超过缓冲区的容量,接收端有可能在缓冲区被填满以后一次性地读取,也可能分成好几次读取。

SOCK_DGRAM

使用 UDP 协议,是一种不可靠的、不按顺序传递的、以追求速度为目的的套接字。数据的发送和接收是同步的,换句话说,接收次数应该和发送次数相同。

socket creation

在 Linux 下使用 <sys/socket.h> 头文件中 socket() 函数来创建套接字,原型为:
int socket(int af, int type, int protocol);
af 为地址族(Address Family), 常用的有 AF_INETAF_INET6 对应IPv4与IPv6。
type 为数据传输方式/套接字类型,常用的有 SOCK_STREAM(流格式套接字/面向连接的套接字) 和 SOCK_DGRAM(数据报套接字/无连接的套接字)。
protocol 表示传输协议,常用的有 IPPROTO_TCPIPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议。可以设为0,会通过type推导出需要什么协议。

int tcp_socket = socket(AF_INET, SOCK_STREAM, 0);
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0); 

bind()

bind() 函数的原型为:
int bind(int sock, struct sockaddr *addr, socklen_t addrlen);
sock 为 socket 文件描述符,addr 为 sockaddr 结构体变量的指针,addrlen 为 addr 变量的大小,可由 sizeof() 计算得出。

int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;  //use IPv4
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  //localhost
serv_addr.sin_port = htons(1234);  //port

bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

这里我们使用 sockaddr_in 结构体,然后再强制转换为 sockaddr 类型.

sockaddr_in vs sockaddr


sockaddrsockaddr_in 的长度相同,都是16字节,只是将IP地址和端口号合并到一起,用一个成员 sa_data 表示。要想给 sa_data 赋值,必须同时指明IP地址和端口号,例如”127.0.0.1:80“,遗憾的是,没有相关函数将这个字符串转换成需要的形式,也就很难给 sockaddr 类型的变量赋值,所以使用 sockaddr_in 来代替。这两个结构体的长度相同,强制转换类型时不会丢失字节,也没有多余的字节。可以认为,sockaddr 是一种通用的结构体,可以用来保存多种类型的IP地址和端口号,而 sockaddr_in 是专门用来保存 IPv4 地址的结构体。另外还有 sockaddr_in6,用来保存 IPv6 地址

struct sockaddr_in{
    sa_family_t        sin_family;     //地址族(Address Family) / 地址类型
    uint16_t            sin_port;        //16位的端口号 0-65535
    struct in_addr   sin_addr;       //32位IP地址
    char                  sin_zero[8];    //不使用,一般用0填充
};
struct in_addr{
    // inside <netinet/in.h>
    // typedef uint32_t in_addr_t 
    in_addr_t          s_addr;           //32位的IP地址
};

s_addr 是一个整数,而IP地址是一个字符串,所以需要 inet_addr() 函数进行转换
unsigned long ip = inet_addr("127.0.0.1"); // ip will be 16777343

connect()

connect() 函数用来建立连接,它的原型为:
int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen);

listen()

通过 listen() 函数可以让套接字进入被动监听状态,它的原型为:
int listen(int sock, int backlog);
sock 为需要进入监听状态的套接字,backlog 为请求队列的最大长度。当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的,只能把它放进缓冲区,待当前请求处理完毕后,再从缓冲区中读取出来处理。如果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队,直到缓冲区满。这个缓冲区,就称为请求队列(Request Queue)。缓冲区的长度(能存放多少个客户端请求)可以通过 listen() 函数的 backlog 参数指定,如果将 backlog 的值设置为 SOMAXCONN,则为系统最大支持值。当请求队列满时,就不再接收新的请求,对于 Linux,客户端会收到 ECONNREFUSED 错误。注意:listen() 只是让套接字处于监听状态,并没有接收请求。接收请求需要使用 accept() 函数。

accept()

当套接字处于监听状态时,可以通过 accept() 函数来接收客户端请求。它的原型为:
int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);
accept() 返回一个新的套接字来和客户端通信,addr 保存了客户端的IP地址和端口号,而 sock 是服务器端的套接字,要注意区分。后面和客户端通信时,要使用这个新生成的套接字,而不是原来服务器端的套接字。需要说明的是:listen() 只是让套接字进入监听状态,并没有真正接收客户端请求,listen() 后面的代码会继续执行,直到遇到 accept()。accept() 会阻塞程序执行(后面代码不能被执行),直到有新的请求到来。

write() send() read() recv()

write 函数原型:
ssize_t write(int fd, const void*buf,size_t nbytes)
write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节数.失败时返回-1.并设置errno变量. 在网络程序中,当我们向套接字文件描述符写时有两可能:

  1. write的返回值大于0,表示写了部分或者是全部的数据. 这样我们用一个while循环来不停的写入,但是循环过程中的buf参数和nbyte参数得由我们来更新。也就是说,网络写函数是不负责将全部数据写完之后在返回的。
  2. 返回的值小于0,此时出现了错误.我们要根据错误类型来处理.如果错误为EINTR表示在写的时候出现了中断错误.如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接)。

read 函数原型:
ssize_t read(int fd,void *buf,size_t nbyte)
read函数是负责从fd中读取内容。当读成功时,read返回实际所读的字节数,如果返回的值是0 表示已经读到文件的结束了,小于0表示出现了错误。如果错误为EINTR说明读是由中断引起 的, 如果是ECONNREST表示网络连接出了问题.

recvsend函数提供了和read和write差不多的功能.不过它们提供了第四个参数来控制读写操作:
int recv(int sockfd,void *buf,int len,int flags)
int send(int sockfd,void *buf,int len,int flags)
前面的三个参数和read,write一样,第四个参数可以是0或者是以下的组合:

  1. MSG_DONTROUTE:是send函数使用的标志.这个标志告诉IP.目的主机在本地网络上面,没有必要查找表.这个标志一般用网络诊断和路由程序里面.
  2. MSG_OOB:表示接收和发送带外的数据

(有些传输层协议具有带外(Out of Band,OOB)数据的概念,用于迅速通告对方本端发生的重要事件。因此,带外数据比普通数据(也称带内数据)有更高的优先级,它应该总是立即发送,而不论发送缓冲区是否有排队等待发送的普通数据。带外数据的传输可以使用一条独立的传输层连接,也可以映射到传输普通数据的连接中,TCP采用的是后者(postscript:前者应该怎么实现,应该比较复杂和消耗性能)。在实际应用中,带外数据的使用很少见,已知的仅有telnet、ftp等远程非活跃程序。

  1. MSG_PEEK:是recv函数的使用标志,表示只是从系统缓冲区中读取内容,而不清除系统缓冲区的内容.这样下次读的时候,仍然是一样的内容.一般在有多个进程读写数据时可以使用这个标志.
  2. MSG_WAITALL是recv函数的使用标志,表示等到所有的信息到达时才返回.使用这个标志的时候recv回一直阻塞,直到指定的条件满足,或者是发生了错误. 1)当读到了指定的字节时,函数正常返回.返回值等于len 2)当读到了文件的结尾时,函数正常返回.返回值小于len 3)当操作发生错误时,返回-1,且设置错误为相应的错误号(errno)。

如果flags为0,则和read,write一样的操作。

workflow

TCP:

UDP:

socket缓冲区

每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。TCP协议独立于 write()/send() 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取。这些I/O缓冲区特性可整理如下:

  1. I/O缓冲区在每个TCP套接字中单独存在;
  2. I/O缓冲区在创建套接字时自动生成;
  3. 即使关闭套接字也会继续传送输出缓冲区中遗留的数据;
  4. 关闭套接字将丢失输入缓冲区中的数据。

可以通过getsockoptsetsockopt来获取以及设置socket的某个特性。如下设置发送区buffer

//SOL_SOCKET is the socket layer itself. It is used for options that are protocol independent.
getsockopt(socketFd, SOL_SOCKET, SO_SNDBUF, &bufSize, &bufSizeLen);
setsockopt(scoktetFd, SOL_SOCKET, SO_SNDBUF, &bufSize, sizeof(bufSize));

对于TCP套接字(默认阻塞),当使用 write()/send() 发送数据时:

  1. 首先会检查缓冲区,如果缓冲区的可用空间长度小于要发送的数据,那么 write()/send() 会被阻塞(暂停执行),直到缓冲区中的数据被发送到目标机器,腾出足够的空间,才唤醒 write()/send() 函数继续写入数据。
  2. 如果TCP协议正在向网络发送数据,那么输出缓冲区会被锁定,不允许写入,write()/send() 也会被阻塞,直到数据发送完毕缓冲区解锁,write()/send() 才会被唤醒。
  3. 如果要写入的数据大于缓冲区的最大长度,那么将分批写入。
  4. 直到所有数据被写入缓冲区 write()/send() 才能返回。

当使用 read()/recv() 读取数据时:

  1. 首先会检查缓冲区,如果缓冲区中有数据,那么就读取,否则函数会被阻塞,直到网络上有数据到来。
  2. 如果要读取的数据长度小于缓冲区中的数据长度,那么就不能一次性将缓冲区中的所有数据读出,剩余数据将不断积压,直到有 read()/recv() 函数再次读取。
  3. 直到读取到数据后 read()/recv() 函数才会返回,否则就一直被阻塞。

getsockopt setsockopt

#include <sys/types.h>
#include <sys/socket.h>

int getsockopt(int sockfd, int level, int optname,void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);

sockfd:指向一个打开的套接字描述符
level:通用套接字代码
optname:选项名字
optval:指向某个变量的指针,setsockopt从optval中取得选项待设置的新值,getsockopt把已获取的选项当前值存放在optval中。
optlen:*optval的大小由optlen指定,setsockopt是一个值参数,getsockopt是一个值-结果参数。

成功执行时,返回0。失败返回-1,errno被设为以下的某个值

  1. EBADF:sock不是有效的文件描述词
  2. EFAULT:optval指向的内存并非有效的进程空间
  3. EINVAL:在调用setsockopt()时,optlen无效
  4. ENOPROTOOPT:指定的协议层不能识别选项
  5. ENOTSOCK:sock描述的不是套接字

level指定控制套接字的层次.可以取三种值:

  1. SOL_SOCKET:通用套接字选项.
  2. IPPROTO_IP:IP选项.
  3. IPPROTO_TCP:TCP选项.
optname 说明 数据类型
SOL_SOCKET level
SO_BROADCAST 允许发送广播数据 int
SO_DEBUG 允许调试 int
SO_DONTROUTE 不查找路由 int
SO_ERROR 获得套接字错误 int
SO_KEEPALIVE 保持连接 int
SO_LINGER 延迟关闭连接 struct linger
SO_OOBINLINE 带外数据放入正常数据流 int
SO_RCVBUF 接收缓冲区大小 int
SO_SNDBUF 发送缓冲区大小 int
SO_RCVLOWAT 接收缓冲区下限 int
SO_SNDLOWAT 发送缓冲区下限 int
SO_RCVTIMEO 接收超时 struct timeval
SO_SNDTIMEO 发送超时 struct timeval
SO_REUSERADDR 允许重用本地地址和端口 int
SO_TYPE 获得套接字类型 int
SO_BSDCOMPAT 与BSD系统兼容 int
IPPROTO_IP level
IP_HDRINCL 在数据包中包含IP首部 int
IP_OPTINOS IP首部选项 int
IP_TOS 服务类型 int
IP_TTL 生存时间 int
IPPRO_TCP level
TCP_MAXSEG TCP最大数据段的大小 int
TCP_NODELAY 不使用Nagle算法 int

总结到这里想起来一个知识点,即我们要disable nagle算法.因为它会合并小的网络包,对于高频来讲,我们是不希望有延迟的,希望立刻发送。

int flags =1;
setsockopt(sfd, SOL_TCP, TCP_NODELAY, &flags, sizeof(flags));

另外,交易所也需要取消delay ack,这样能快速回应客户。