socket编程API

字节序

  1. 主机大多为小端,即低字节在低地址
  2. 网络大多为大端,即高字节在低地址
h:主机host,n:网络network,l:unsigned long,s:unsigned short
htonl
htons
ntohl
ntohs

socket地址

对于不同协议有不同的格式,使用时强转
比如IPv6、IPv4有地址和端口号,UNIX本地是文件路径名
如果要在网络中传输,初始化时记得转换字节序

socket

创建socket

int socket(int domain, int type, int protocol);
//返回值:socketfd,文件描述符,错误返回-1设置errno
//domain:协议族,IPV4/IPV6/UNIX(这个不是参数,是参数的意义)
//type:服务类型,流式/报文
//protocl:协议,前两个基本上已经确定了第三个参数的选择,通常选0默认协议

bind

命名socket,为socket绑定socket地址
通常服务器需要命名socket(应该是指明端口,服务大多有公认对应的默认端口),而客户端不需要,匿名由系统分配

int bind(int sockfd, const struct sockaddr* my_addr, socklen_t arrlen);
//返回值:成功0,失败-1和errno
//my_addr:socket地址,根据选择的协议会有不同的格式(参考man手册),使用时强转
//arrlen:my_addr的长度,sizeof即可
  1. 想要知道内核绑定的临时端口,需要调用getsockname,因为my_addr是const类型的

listen

转换状态(CLOSED-->LISTEN),设置已连接队列长度(以前是半连接队列长度+已连接队列长度)

int listen(int sockfd, int backlog);
//返回值:成功0,失败-1和errno
//sockfd:就是创建socket得到的文件描述符
//backlog:完全连接状态的socket队列的长度上限(半连接队列长度由内核参数定义)
  1. 半连接队列上的项会保留到三次握手成功移到已连接队列,或者超时还未收到第三次握手ACK(1个RTT)
  2. 当半连接队列已满,TCP会忽略新SYN,也不会发送RST,让主动连接方的重传机制进行处理
  3. 收到第三个握手报文,将该项移到已连接队列,利用accept获取
  4. 使用select时,监听sockfd不需要设置为非阻塞,因为select为它进行阻塞,返回后accept也不会阻塞

accept

服务器从已连接队列中接受一个连接,生成连接socket(没有则阻塞)

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
//返回值:连接socket,文件描述符
//sockfd:创建出来socket得到的文件描述符
//addr:获取被接受连接的远端socket地址
//addrlen:socket地址的长度
注意第二个和第三个参数都是指针,表示获取
  1. listen过程中三次握手成功就会在已连接队列添加新项,accept只是取出
  2. 注意返回的是连接socket,之后发送、接收数据都通过这个连接socket
  3. 返回socket是阻塞还是非阻塞的?以后测试
  4. 如果服务器建立连接后,accept之前客户端发送RST:不同系统处理方式不同,Linux依旧成功取得连接socket,read时会出错

connect

客户端发起连接

int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
//返回值:成功为0,失败-1和errno
//sockfd:创建出来socket得到的文件描述符,使用了connect会默认命名(相当于bind了)
//serv_addr:服务器监听的socket地址
//addrlen:socket地址的长度
  1. (默认阻塞)连接建立成功或失败才会返回
  2. errno=ETIMEOUT,超时:TCP客户经过5次重连后(默认重传5次,由1s开始每次重传时间翻倍)没有收到SYN响应
  3. errno=ECONNREFUSED,收到RST:服务器没有监听该端口
  4. connect失败后,该sockfd必须调用close关闭,不可再使用(应该是套接字状态改变了)。需要再次发起连接的话重新创建socket
  5. 可以设置(非阻塞),由IO复用进行连接
  6. sockfd可写,连接建立成功
  7. sockfd可读可写(不能直接判断)
  • 调用getpeername,查看是否能得到远端socket地址
  • 调用connect,errno=EISCONN表明第一次connect建立成功

close

关闭连接

int close(int fd);
//返回值:
//fd:待关闭的socket,不过这里只是socket引用数-1,需要引用数为0才会关闭连接
  • 特点:立即返回,TCP模块负责把残留数据发送

shutdown

关闭连接

int shutdown(int sockfd, int howto);
//返回值:成功为0,失败-1和errno
//sockfd:带关闭的socket
//howto:shutdown的行为,关闭读/关闭写(半连接)/关闭读写

send、recv(TCP专用)

TCP流数据的读写

size_t recv(int sockfd, void *buf, size_t len, int flags);
//返回值:读到数据的长度,对方已关闭0,出错-1和errno
//sockfd:连接socket
//buf:缓冲区位置
//len:缓冲区大小
//flags:数据收发的额外控制

size_t send(int sockfd, const void *buf, size_t len, int flags);
//返回值:写入数据的长度,对方已关闭0,出错-1和errno
//sockfd:连接socket
//buf:缓冲区位置
//len:缓冲区大小
//flags:数据收发的额外控制
  • 阻塞send
    • 正在发送,阻塞,不允许写入
    • len大于发送缓存,send阻塞,等待协议栈收到ACK腾出足够大小的发送缓存
    • len小于等于发送缓存,拷贝完毕返回
    • len大于发送缓存总长度,分批写入
    • PS:成功返回实际拷贝的字节数
    • PS:send所有拷贝完毕就马上返回了,不需要等待ACK响应
  • 非阻塞send
    • 尽力拷贝,返回拷贝成功字节数
    • 出错-1和errno=EAGAIN
  • 阻塞recv
    • 缓冲区没有数据,阻塞
  • PS:阻塞时间可以在socket选项更改

sendto、recvfrom(UDP专用)

UDP数据报的读写,最后两个参数为NULL也可用于TCP读写

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen *addrlen);
//返回值:成功读取数据长度,出错-1
//sockfd:创建socket返回的文件描述符(注意不是连接socket)
//buf:缓冲区位置
//len:缓冲区大小
//flags:数据收发的额外控制
//src_addr:获取发送端的socket地址(因为没有连接)
//addrlen:socket地址的长度,注意是指针

ssize_t sendto(int sockfd, void *buf, size_t len, int flags, struct sockaddr *dest_addr, socklen addrlen);
//返回值:成功发送数据长度,出错-1
//sockfd:创建socket返回的文件描述符(注意不是连接socket)
//buf:缓冲区位置
//len:缓冲区大小
//flags:数据收发的额外控制
//dest_addr:接收端的socket地址(因为没有连接)
//addrlen:socket地址的长度

ps:关于ssize_t:和size_t、long一样,取决于不同系统、编译器,不过ssize_t是有符号的,size_t是无符号的

sendmsg、recvmsg(通用数据读写)

不仅能用于TCP流数据,也能用于UDP数据报

ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
ssize_t sendmsg(int sockfd, struct msghdr *msg, int flags);

//参数和上面一样,主要是msghdr结构体,其实里面的东西和上面两组函数类似,man手册查看

获取地址

根据连接socket获取socket地址

int getsockname(int sockfd, struct sockaddr *address, socklen_t *address_len);  //本端
int getpeername(int sockfd, struct sockaddr *address, socklen_t *address_len);  //远端
//返回值:成功为0,失败-1和errno
//sockfd:连接socket
//address:获取socket地址
//address_len:socket地址长度,注意是指针
  1. 在没有调用bind的TCP客户上,connect成功返回后,可以调用getsockname查看系统分配的socket地址

socket读写

  1. 下列情况下socket可读:
  • socket内核接收缓冲区的字节数大于或等于其低水位标记SO_RCVLOWAT;
  • socket通信的对方关闭连接FIN,此时该socket可读,但是一旦读该socket,会立即返回0(可以用这个方法判断client端是否断开连接);
  • 监听socket上有新的连接请求;
  • socket上有未处理的错误。读返回-1,从errno中取错误
  1. 下列情况下socket可写:
  • socket内核发送缓冲区的可用字节数大于或等于其低水位标记SO_SNDLOWAT;
  • socket的读端关闭,此时该socket可写,一旦对该socket进行操作,该进程会收到SIGPIPE信号;
    • SIGPIPE信号:客户端关闭了链接,服务端第一次写成功,客户端返回RST,服务端第二次写就会有SIGPIPE信号;服务端需要调用信号处理函数忽略SIGPIPE信号
  • socket使用connect连接成功之后;
  • socket上有未处理的错误。写返回-1,从errno中取错误

socket选项设置

和fcntl操作文件描述符类似

int getsockopt(int sockfd, int level, int option_name, void *option_value, socklen_t* restrict option_len);
int setsockopt(int sockfd, int level, int option_name, const void *option_value, socklen_t option_len);
//返回值:成功为0,失败-1和errno
//sockfd:指定操作的socket
//level:指明协议,通用/IPv4/IPv6/TCP,不同协议有不同的选项
//option_name:选项名
//option_value:选项值
//option_len:选项长度,注意get是指针
//restrict:用于告诉编译器,对象已经被指针所引用,不能通过除该指针外所有其他直接或间接的方式修改该对象的内容

常用的几个option_value

  • SO_KEEPALIVE:发送周期性保活报文以维持连接,2小时内没发生数据传输,发送心跳包
    • 收到RST,关闭
    • 收到ACK,正常,重启2小时定时器
    • 什么都没收到,重传时间为75s,重传8次,总共9个心跳包,说明最长判断时间时2小时11分15秒
    • 切断网线:探测时会收到路由器的ICMP差错报文
    • 主机崩溃:重传后返回超时错误
    • 主机崩溃重启:收到RST
  • TCP_NODELAY:禁止nagle算法
  • TCP_MAXSEG:TCP最大报文端大小,只对监听socket有效(因为accept得到连接socket至少已经两次握手了)
  • IP_TTL:存活时间
  • SO_RCVBUF、SO_SNDBUF:TCP接收缓冲区和发送缓冲区的大小
  • SO_RCVLOWAT、SO_SNDLOWAT:TCP接收缓冲区可读数据和发送缓冲区空闲空间的低水位
  • SO_REUSEADDR:重用TIME_WAIT的socket,和内核参数recyle参数相同,没有TIME_WAIT状态
  • SO_LINGER:控制close关于面向连接协议的行为,有两个参数l_onoff(开启关闭),l_linger(滞留时间)
    • l_onoff=0:默认行为,调用后马上返回,发送缓冲区剩余内容
    • l_onoff!=0,l_linger=0:异常终止,调用后马上返回,清除发送缓冲区剩余内容,发送RST报文
    • l_onoff!=0,l_linger>0:
      • 阻塞socket:等待l_linger时间,发送缓冲区剩余内容,直到收到ACK报文;超时返回-1和errno
      • 非阻塞socket:close马上返回,根据返回值和errno判断是否发送完毕
  • PS连接socket会继承监听socket的部分设置

gethostbyname和gethostbyaddr

gethostbyname根据主机名获取主机的完整信息(就是DNS的机制)
gethostbyaddr根据IP地址获取主机的完整信息

零拷贝

零拷贝消除了将用户空间数据拷贝内核空间的步骤,依靠硬件甚至连内核空间内文件描述符之间的拷贝也消除

sendfile

//ssize_t sendfile(int out_fd, int in_fd, off_t *offet, size_t count);
//返回值:成功传输的字节数,失败为-1和errno
//out_fd:待写入内容的文件描述符,**必须是socket的fd**
//in_fd:待读出内容的文件描述符,**必须是指向真实文件的fd**
//offet:读入文件流从哪个位置开始
//count:传输字节数

mmap

mmap申请一段内存空间,将用户空间和这段内存空间绑定,可以用作共享内存,或将文件映射到这段内存空间

void* mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
返回值:
start:用户空间中用于映射的起始地址
length:指定内存段的大小
prot:内存段的访问权限,读写执行访问
flags:控制内存段内容被修改后程序的行为
fd:被映射文件对应的文件描述符
offset:从文件的何处开始映射

splice

用于两个文件描述符之间移动数据,必须至少有一个文件描述符是管道

ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags);
//返回值:成功移动的字节数,失败-1和errno
//fd_in:待输入数据的文件描述符
//off_in:从输入数据流何处开始读取数据,管道文件必须NULL
//fd_out:待输出数据的文件描述符
//off_out:从输出数据流何处开始输出数据,管道文件必须NULL
//len:移动数据的长度
//flags:控制数据如何移动

tee

两个管道文件描述符之间复制数据

ssize_t tee(int fd_in, loff_t *off_in, size_t len, unsigned int flags);
//和splice相同
posted @ 2021-03-25 23:32  肥斯大只仔  阅读(142)  评论(0编辑  收藏  举报