Socket通信原理学习

1.TCP/IP 与UDP

TCP/IP(Transmission Control Protocol /Internet protocol) ,传输控制协议与网间协议,是一个工业标准的协议集,是为广域网(WANs)设计的。
UDP(User Data Protocol)是与TCP相对应的协议。

在这里插入图片描述
TCP 是面向连接的传输协议,建立连接时要经过三次握手,断开连接时要经过四次握手,中间传输数据时也要回复 ACK 包确认,多种机制保证了数据能够正确到达,不会丢失或出错。

UDP 是非连接的传输协议,没有建立连接和断开连接的过程,它只是简单地把数据丢到网络中,也不需要 ACK 包确认。

UDP 传输数据就好像我们邮寄包裹,邮寄前需要填好寄件人和收件人地址,之后送到快递公司即可,但包裹是否正确送达、是否损坏我们无法得知,也无法保证。UDP 协议也是如此,它只管把数据包发送到网络,然后就不管了,如果数据丢失或损坏,发送端是无法知道的,当然也不会重发。

既然如此,TCP 应该是更加优质的传输协议吧?
如果只考虑可靠性,TCP 的确比 UDP 好。但 UDP 在结构上比 TCP 更加简洁,不会发送 ACK 的应答消息,也不会给数据包分配 Seq 序号,所以 UDP 的传输效率有时会比 TCP 高出很多,编程中实现 UDP 也比 TCP 简单。

UDP 的可靠性虽然比不上TCP,但也不会像想象中那么频繁地发生数据损毁,在更加重视传输效率而非可靠性的情况下,UDP 是一种很好的选择。比如视频通信或音频通信,就非常适合采用 UDP 协议;通信时数据必须高效传输才不会产生“卡顿”现象,用户体验才更加流畅,如果丢失几个数据包,视频画面可能会出现“雪花”,音频可能会夹带一些杂音,这些都是无妨的。

与 UDP 相比,TCP 的生命在于流控制,这保证了数据传输的正确性。

最后需要说明的是:TCP 的速度无法超越 UDP,但在收发某些类型的数据时有可能接近 UDP。例如,每次交换的数据量越大,TCP 的传输速率就越接近于 UDP。

2 Socket

在这里插入图片描述

Socket是应用层与TCP/IP协议族通信的中间软件抽象层,是一组接口。在设计模式中,socket其实就是一个门面模式,他把复杂的TCP/IP协议族隐藏在Socket接口后面,for User, 一组简单的接口就是全部,由socket去组织数据以符合指定的协议。
在这里插入图片描述
从TCP/IP通信看:

  • Server :
    初始化socket 、bind port、listen port 、调用accept 阻塞、等待client 连接。

  • Client:
    初始化socket 、建立连接、请求数据、读取数据,关闭连接。

3 网络中的进程之间如何通信?

本地的进程间通信(IPC)主要可以总结为以下4类:

  • 信息传递(管道、FIFO、消息队列)
  • 同步(互斥量、条件变量、读写锁、文件和写记录锁、信号量)
  • 共享内存(匿名、具名)
  • 远程过程调用(Solaris门、Sun RPC)

网络中如何唯一标识进程?
网络层的 IP地址 -> 唯一标识网络中的主机
传输层的 协议和端口-> 唯一标识主机中的进程 。

一切皆socket

socket一词的起源
在组网领域的首次使用是在1970年2月12日发布的文献IETF RFC33中发现的,撰写者为Stephen Carr、Steve Crocker和Vint Cerf。根据美国计算机历史博物馆的记载,Croker写道:“命名空间的元素都可称为套接字接口。一个套接字接口构成一个连接的一端,而一个连接可完全由一对套接字接口规定。”计算机历史博物馆补充道:“这比BSD的套接字接口定义早了大约12年。”

socket缓冲区

每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区
write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。

TCP协议独立于 write()/send() 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。

read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取。
在这里插入图片描述
这些I/O缓冲区特性可整理如下:

  1. I/O缓冲区在每个TCP套接字中单独存在;
  2. I/O缓冲区在创建套接字时自动生成;
  3. 即使关闭套接字也会继续传送输出缓冲区中遗留的数据;
  4. 关闭套接字将丢失输入缓冲区中的数据。
    输入输出缓冲区的默认大小一般都是 8K,可以通过 getsockopt() 函数获取:
unsigned optVal;
int optLen = sizeof(int);
getsockopt(servSock, SOL_SOCKET, SO_SNDBUF, (char*)&optVal, &optLen);
printf("Buffer length: %d\n", optVal);

阻塞模式

对于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() 函数才会返回,否则就一直被阻塞。

这就是TCP套接字的阻塞模式。所谓阻塞,就是上一步动作没有完成,下一步动作将暂停,直到上一步动作完成后才能继续,以保持同步性

TCP协议的粘包问题 (数据的无边界性)

上节我们讲到了socket缓冲区和数据的传递过程,可以看到数据的接收和发送是无关的,read()/recv() 函数不管数据发送了多少次,都会尽可能多的接收数据。也就是说,read()/recv() 和 write()/send() 的执行次数可能不同。

例如,write()/send() 重复执行三次,每次都发送字符串"abc",那么目标机器上的 read()/recv() 可能分三次接收,每次都接收"abc";也可能分两次接收,第一次接收"abcab",第二次接收"cabc";也可能一次就接收到字符串"abcabcabc"。

假设我们希望客户端每次发送一位学生的学号,让服务器端返回该学生的姓名、住址、成绩等信息,这时候可能就会出现问题,服务器端不能区分学生的学号。例如第一次发送 1,第二次发送 3,服务器可能当成 13 来处理,返回的信息显然是错误的。

这就是数据的“粘包”问题,客户端发送的多个数据包被当做一个数据包接收。也称数据的无边界性,read()/recv() 函数不知道数据包的开始或结束标志(实际上也没有任何开始或结束标志),只把它们当做连续的数据流来处理。
具体问题解决见:

4. socket的基本操作

既然socket是“open-write/read-close”模式的实现,socket就提供了这些操作对应的函数接口。

1.socket函数

int socket(int domain, int type , int protocol);
  • socket:函数创建一个socket描述符,唯一标识一个socket。
  • domain:协议域,协议族。常见的协议族有: AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,unix域socket) 、AF_ROUTE等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET对应IPv4地址(32位)和端口号(16位)、AF_UNIX决定了要用一个绝对路径名做地址。
  • type :指定socket类型。常用的类型:SOCKET_STREAM、SOCKET_DGRAM 、SOCKET_RAM、 SOCKET_PACKET、 SOCKET_SEQPACKET等等。
  • protocol:指定协议。常用的协议:IPPROTO_TCP、 IPPROTO_UDP、 IPPROTO_SCTP 、IPPROTO_TIPC等,他们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。
    注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。
    当我们调用socket函数获取的socket描述字存在于协议族(address family,AF_XXX )空间,但没有具体的地址。如果想给他赋具体的地址,就必须调用bind函数,否则当调用connect、listen时系统会自动分配一个端口。

2. bind函数

把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把ipv4/ipv6地址和端口号组合赋值给socket。

int bind(int socketfd, const struct sockaddr* addr,socklen_t addrlen);
  • socketfd :socket描述字
  • addr:const struct sockaddr 指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,如ipv4对应的是:
struct sockaddr_in{
    sa_family_t  sin_family;
    in_port_t sin_port;
    struct in_addr_in sin_addr; 
};
struct in_addr{
    uint32_t s_addr;
};

ipv6对应的是:

struct sockaddr_in6{
    sa_family_t  sin6_family;
    in_port_t sin6_port;
    uint32_t sin6_flowinfo;
    struct in6_addr sin6_addr;
    uint32_t sin6_scope_id;
};
struct in6_addr{
     unsigned char s6_addr[16];
};
UNIX域对应:
#define UNIX_PATH_MAX 108
struct sockaddr_in{
   sa_family_t sun_family;
   char sun_path[UNIX_PATH_MAX];
};

sockaddr结构体

struct sockaddr {  
 unsigned short sa_family;   // 通信类型,最常用的值是 "AF_INET"
 char sa_data[14];               // 14字节,包含套接字中的目标地址和端口信息         
 }; 

sockaddr 和 sockaddr_in的相互关系
一般先把sockaddr_in变量赋值后,强制类型转换后传入用sockaddr做参数的函数
sockaddr_in用于socket定义和赋值
sockaddr用于函数参数

  • 网络字节顺序 (Network Byte Order) NBO
    结构体的sin_port和sin_addr都必须是NBO

  • 本机字节顺序 (Host Byte Order) HBO
    一般可视化的数字都是HBO

    NBO,HBO二者转换
    inet_addr() 将字符串点数格式地址转化成无符号长整型(unsigned long s_addr s_addr;)
    inet_aton() 将字符串点数格式地址转化成NBO
    inet_ntoa () 将NBO地址转化成字符串点数格式
    htons() “Host to Network Short”
    htonl() “Host to Network Long”
    ntohs() “Network to Host Short”
    ntohl() “Network to Host Long”
    常用的是htons(),inet_addr()正好对应结构体的端口类型和地址类型

  • 三种给socket赋值地址的方法

inet_aton(server_addr_string,&myaddr.sin_addr);
myaddr.sin_addr.s_addr = inet_addr("132.241.5.10");
// INADDR_ANY转不转NBO随便
myaddr.sin_addr.s_addr = htons(INADDR_ANY);  
myaddr.sin_addr.s_addr = INADDR_ANY;
  • 两种给socket 赋值端口的方法
 #define MYPORT 3490 
	myaddr.sin_port = htons(MYPORT);
	// 0(随机端口)转不转NBO随便
	myaddr.sin_port = htons(0);
	myaddr.sin_port = 0;  

htons/l 和 ntohs/l 等数字转换都不能用于地址转换,因为地址都是点数格式,所以地址只能采用数字/字符串转换如inet_aton,inet_ntoa ;
唯一可以用于地址转换的htons是针对INADDR_ANY

 cliaddr.sin_addr.s_addr = htons(INADDR_ANY)
  • inet_addr()与inet_aton()的区别
    • inet_addr() 是返回值型
      struct sockaddr_in ina; ina.sin_addr.s_addr = inet_addr("132.241.5.10");
    • inet_aton() 是参数指针型
      struct sockaddr_in ina; inet_aton("132.241.5.10",&ina.sin_addr);
    • inet_ntoa() 将NBO地址转化成字符串点数格式
      a1 = inet_ntoa(ina.sin_addr); printf("address 1: %s\n",a1);
    • ==inet_addr()的缺陷:必须对-1做检测处理 ==
      因为inet_addr()的结果是整型,而发生错误时返回-1。
      而 ina.sin_addr.s_addr是unsigned long型
      -1在long short显示成111111111,和IP地址255.255.255.255相符合!会被误认为广播地址!
  • addrlen:对应的是地址的长度。
    通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
网络字节序和主机字节序

主机字节序(HBO):就是我们常说的大端小端模式:不同的CPU有不同的字节序类型,这些字节数是指整数在内存中保存的顺序,这就叫做主机序。
网络字节序(NBO):4个字节的32bit值以下面的次序传输:0-7bit,8-15bit,16-31bit.这种传输次序叫做大端字节序。
由于TCP/IP首部中所有的二进制整数在网络传输时都要求以这种次序,它又叫做网络字节序。
字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。
所以: 在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是Big-Endian。由于 这个问题曾引发过血案!公司项目代码中由于存在这个问题,导致了很多莫名其妙的问题,所以请谨记对主机字节序不要做任何假定,务必将其转化为网络字节序再 赋给socket。

3.listen、connect函数

作为一个服务器,调用socket、bind后会调用listen来监听这个socket,如果客户端此时调用connect发出连接请求,服务器就会接收到这个请求。

int listen(int sockfd,int backlog);
int connect (int sockfd,struct sockaddr * serv_addr,int addrlen);

listen函数的第一个参数是要监听的sockrt描述字,第二个参数是相应socket可以排队的最大连接数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型,等待客户的连接请求。
connect函数的第一个参数即为客户端的socket描述字,第二个参数位服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数建立与TCP服务器的连接。

4.accept函数

TCP服务器依次调用socket、bind、listen后就会监听指定的socket地址。当客户端通过connect向服务器发送连接请求。服务器监听到连接请求后,就会调用accept接受请求,建立连接。

int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
  • sockfd,socket描述字
  • addr 指向struct sockaddr的指针,返回客户端的协议地址
  • addrlen 协议地址长度。

如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。
注意:accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭

5.read、write等函数

网络I/O操作有以下几组;

  • read/write
  • recv/send
  • readv/writev
  • recvmsg/sendmsg
  • recvfrom/sendto
    具体函数声明如下:
   ssize_t read(int fd, void *buf, size_t count);  
   ssize_t write(int fd, const void *buf, size_t count);   
   ssize_t send(int sockfd, const void *buf, size_t len, int flags);   
   ssize_t recv(int sockfd, void *buf, size_t len, int flags);   
   ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,  const struct sockaddr *dest_addr, socklen_t addrlen);  
   ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,  struct sockaddr *src_addr, socklen_t *addrlen);   
   ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);   
   ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
5.1 read/write

read函数是负责从fd中读取内容.当读成功时,read返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了,小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题。
write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节 数。失败时返回-1,并设置errno变量。在网络程序中,当我们向套接字文件描述符写时有俩种可能。

  • 1)write的返回值大于0,表示写了部分或者是 全部的数据。
  • 2)返回的值小于0,此时出现了错误。我们要根据错误类型来处理。
  • 如果错误为EINTR表示在写的时候出现了中断错误。
  • 如果为EPIPE表示 网络连接出现了问题(对方已经关闭了连接)。
5.2 recv/send
ssize_t send(int sockfd, const void *buf, size_t len, int flags);   
ssize_t recv(int sockfd, void *buf, size_t len, int flags);   

第四个参数可以是0或者是下面的组合

flags描述
MSG_DONTROUTE不查找表
MSG_OOB接受或者发送带外数据
MSG_PEEK查看数据,并不从系统缓冲区移走数据
MSG_WAITALL等待所有数据

MSG_DONTROUTE: 是send函数使用的标志.这个标志告诉IP.目的主机在本地网络上面,没有必要查找表.这个标志一般用网络诊断和路由程序里面.
MSG_OOB:表示可以接收和发送带外的数据.关于带外数据我们以后会解释的.
MSG_PEEK: 是recv函数的使用标志,表示只是从系统缓冲区中读取内容,而不清除系统缓冲区的内容.这样下次读的时候,仍然是一样的内容.一般在有多个进程读写数据时可以使用这个标志.
MSG_WAITALL: 是recv函数的使用标志,表示等到所有的信息到达时才返回.使用这个标志的时候recv回一直阻塞,直到指定的条件满足,或者是发生了错误. 1)当读到了指定的字节时,函数正常返回.返回值等于len 2)当读到了文件的结尾时,函数正常返回.返回值小于len 3)当操作发生错误时,返回-1,且设置错误为相应的错误号(errno)
MSG_NOSIGNAL is a flag used by send() in some implementations of the Berkeley sockets API.

该标志要求实现在另一端断开连接时,针对面向流的套接字上的错误不发送SIGPIPE信号。 EPIPE错误仍然照常返回。
尽管它在某些Berkely套接字API(尤其是Linux)中使用,但在某些人称为参考实现的FreeBSD中却不存在,它使用套接字选项SO_NOSIGPIPE?
对于服务器端,我们可以使用这个标志。目的是不让其发送SIG_PIPE信号,导致程序退出。

如果flags为0,则和read,write一样的操作.还有其它的几个选项,不过我们实际上用的很少,可以查看 Linux Programmer’s Manual得到详细解释
当recv/send的flag参数设置为0时,则和read/write是一样的。
如果有如下几种需求,则read/write无法满足,必须使用recv/send:

  1. 为接收和发送进行一些选项设置
  2. 从多个客户端中接收报文
  3. 发送带外数据(out-of-band data)
  • read/write 与recv /send的区别
    recv()/send() 仅适用于套接字描述符,并允许您为实际操作指定某些选项。这些功能稍微更专业(例如,您可以设置一个标志来忽略SIGPIPE或发送带外消息…) .
    函数read()/ write()是适用于所有描述符的通用文件描述符函数。不仅仅是套接字描述符.

最初在何处引入recv和send的原因是,并非所有数据报概念都可以映射到流的世界。读写将所有内容视为数据流,无论是管道,文件,设备(例如串行端口)还是套接字。但是,套接字仅使用TCP时才是实际流。如果使用UDP,则它更像是块设备。但是如果双方都像流一样使用它,它将像流一样工作,并且您甚至无法使用写调用发送空的UDP数据包,因此不会出现这种情况。

6.close

int close(int fd);
close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。
注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求

7.setsockopt

#include <sys/types.h>          /* See NOTES */\
#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);

getsockopt()和setsockopt()操纵文件描述符sockfd所引用的套接字的选项。选项可能存在于多个协议级别。它们始终位于最上层的套接字级别。
操作套接字选项时,必须指定该选项所在的级别和该选项的名称。要在套接字API级别上操作选项,请将level指定为SOL_SOCKET。
要在其他任何level上操作选项,请提供控制该选项的适当协议的协议号。例如,为了指示选项将由TCP协议解释,应将level设置为TCP的协议号;
参数optvaloptlen用于访问选项的值。对于getsockopt(),它们标识一个缓冲区,在该缓冲区中将返回所请求选项的值。对于getsockopt(),optlen是一个值结果参数,最初包含optval指向的缓冲区的大小,并在返回时进行修改以指示返回值的实际大小。如果不提供或返回任何选项值,则optval可以为NULL。
Optname和任何指定的选项将不解释地传递到适当的协议模块以进行解释。包含文件 <sys / socket.h>包含套接字级别选项的定义,如下所述。其他协议级别的选项的格式和名称有所不同。

7.1 setsockopt 用法 – 设置timeout
  • 在TCP连接中,recv等函数默认为阻塞模式(block),即直到有数据到来之前函数不会返回,而我们有时则需要一种超时机制使其在一定时间后返回而不管是否有数据到来,这里我们就会用到setsockopt()函数:
  int  setsockopt(int  s, int level, int optname, void* optval, socklen_t* optlen);

这里我们要涉及到一个结构:

  struct timeval
    {
            time_t tv_sec;
            time_t tv_usec;
    };

这里第一个域的单位为秒,第二个域的单位为微秒。

struct timeval tv_out;
tv_out.tv_sec = 1;
tv_out.tv_usec = 0;

填充这个结构后,我们就可以以如下的方式调用这个函数:
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv_out, sizeof(tv_out));
这样我们就设定了recv()函数的超时机制,当超过tv_out设定的时间而没有数据到来时recv()就会返回0值。

  • 第二个我们要介绍的是多路复用机制,也就是同时监听多个套接字连接。
int select(int n, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);

这里涉及到了fd_set结构:

typedef struct fd_set
{
        u_int fd_count;
        int fd_array[FD_SETSIZE];
}

fd_count为fd_set结构中包含的套接字个数,fd_array唯一个int 数组,包含了我们要监听的套接字。
首先我们需要使用FD_SET将我们要监听的套接字添加到fd_set结构中:

fd_set readfd;
FD_SET(fd, &readfd);

然后我们这样调用select函数:

select(max_fd + 1, &readfd, NULL, NULL, NULL); 
FD_ISSET(fd, &readfd);

其中max_fd为我们要监听的套接字中值最大的一个,同时在调用select是要将其加1,readfd即为我们监听的要进行读操作的套接字连接,第三个参数是我们监听的要进行写操作的套接字连接,第四个参数用于异常,而最后一个参数可以用来设定超时,这里同样使用了struct timeval 结构,可以实现与前面介绍的同样的效果。这里如果连接进来的话select即返回一个大于零的值,然后我们调用FD_ISSET宏来检测具体是那一个套接字有数据进来(FD_ISSET返回非零值)。

  • 最后介绍的是另一种实现非阻塞的方法,这种方法在有些应用中会起到一定作用,尤其是在select()函数监听的套接字个数超过1024个时(因为fd_set结构在大部分UNIX系统中都对其可以监听的套接字个数作了1024的限制,如果要突破这个限制,必须修改头文件并重新编译内核),我们就不能使用select多路复用机制。
    拿recv()函数来说,我们可以这样进行调用:
  recv(fd, buf, sizeof(buf), MSG_DONTWAIT);
注意到我们这里采用了`MSG_DONTWAIT`标志,它的作用是告诉recv()函数如果有数据到来的话就接受全部数据并立刻返回,没有数据的话也是立刻返回,而不进行任何的等待。采用这个机制就可以在多于1024个套接字连接时使用for()循环对全部的连接进行监听。
7.2 关于setsockopt函数选项的一些设置:
  1. closesocket(一般不会立即关闭而经历TIME_WAIT的过程)后想继续重用该socket:
BOOL bReuseaddr=TRUE;
setsockopt(s,SOL_SOCKET ,SO_REUSEADDR,(const char*)&bReuseaddr,sizeof(BOOL));
  1. 如果要已经处于连接状态的soket在调用closesocket后强制关闭,不经历TIME_WAIT的过程:
BOOL bDontLinger = FALSE;
setsockopt(s,SOL_SOCKET,SO_DONTLINGER,(const char*)&bDontLinger,sizeof(BOOL));
  1. 在send(), recv()过程中有时由于网络状况等原因,发收不能预期进行, 而设置收发时限:
int nNetTimeout=1000;//1秒
//发送时限
setsockopt(socket,SOL_S0CKET,SO_SNDTIMEO,(char *)&nNetTimeout,sizeof(int));
//接收时限
setsockopt(socket,SOL_S0CKET,SO_RCVTIMEO,(char *)&nNetTimeout,sizeof(int));
  1. 在send()的时候,返回的是实际发送出去的字节(同步)或发送到socket缓冲区的字节
    (异步);系统默认的状态发送和接收一次为8688字节(约为8.5K);在实际的过程中发送数据
    和接收数据量比较大,可以设置socket缓冲区,而避免了send(),recv()不断的循环收发:
// 接收缓冲区
int nRecvBuf=32*1024;//设置为32K
setsockopt(s,SOL_SOCKET,SO_RCVBUF,(const char*)&nRecvBuf,sizeof(int));
//发送缓冲区
int nSendBuf=32*1024;//设置为32K
setsockopt(s,SOL_SOCKET,SO_SNDBUF,(const char*)&nSendBuf,sizeof(int));
  1. 如果在发送数据的时侯,希望不经历由系统缓冲区到socket缓冲区的拷贝而影响程序的性能:
int nZero=0;
setsockopt(socket,SOL_S0CKET,SO_SNDBUF,(char *)&nZero,sizeof(nZero));
  1. 同上在recv()完成上述功能(默认情况是将socket缓冲区的内容拷贝到系统缓冲区):
int nZero=0;
setsockopt(socket,SOL_S0CKET,SO_RCVBUF,(char *)&nZero,sizeof(int));
  1. 一般在发送UDP数据报的时候,希望该socket发送的数据具有广播特性:
BOOL bBroadcast=TRUE;
setsockopt(s,SOL_SOCKET,SO_BROADCAST,(const char*)&bBroadcast,sizeof(BOOL));

8.在client连接服务器过程中,如果处于非阻塞模式下的socket在connect()的过程中可以设置connect()延时,直到accpet()被呼叫(本函数设置只有在非阻塞的过程中有显著的作用,在阻塞的函数调用中作用不大)

BOOL bConditionalAccept=TRUE;
setsockopt(s,SOL_SOCKET,SO_CONDITIONAL_ACCEPT,(const char*)&bConditionalAccept,sizeof(BOOL));

9.如果在发送数据的过程中(send()没有完成,还有数据没发送)而调用了closesocket(),以前我们一般采取的措施是=="从容关闭"shutdown(s,SD_BOTH),但是数据是肯定丢失了==,如何设置让程序满足具体应用的要求(即让没发完的数据发送出去后在关闭socket)?

struct linger {
u_short l_onoff;
u_short l_linger;
};
linger m_sLinger;
m_sLinger.l_onoff=1;//(在closesocket()调用,但是还有数据没发送完毕的时候容许逗留)
// 如果m_sLinger.l_onoff=0;则功能和2.)作用相同;
m_sLinger.l_linger=5;//(容许逗留的时间为5秒)
setsockopt(s,SOL_SOCKET,SO_LINGER,(const char*)&m_sLinger,sizeof(linger));
  • SO_LINGER
    此选项指定函数close对面向连接的协议如何操作(如TCP)。
    缺省close操作是立即返回,如果有数据残留在套接口缓冲区中则系统将试着将这些数据发送给对方。
    SO_LINGER选项用于控制下述情况的行动:套接口上有排队的待发送数据,且closesocket()调用已执行。
    SO_LINGER选项用来改变此缺省设置。使用如下结构:
struct linger { 
     int l_onoff;  
     int l_linger;  
}; 
  • 有下列三种情况:
    l_onoff为0,则该选项关闭,l_linger的值被忽略,等于缺省情况,close立即返回;
    l_onoff为非0,l_linger为0,则套接口关闭时TCP夭折连接,TCP将丢弃保留在套接口发送缓冲区中的任何数据并发送一个RST 给对方,而不是通常的四分组终止序列,这避免了TIME_WAIT状态;
    l_onoff 为非0,l_linger为非0,当套接口关闭时内核将拖延一段时间(由l_linger决定)。如果套接口缓冲区中仍残留数据,进程将处于睡眠状态,直 到
    • a)所有数据发送完且被对方确认,之后进行正常的终止序列(描述字访问计数为0)
    • b)延迟时间到。此种情况下,应用程序检查close的返回值是 非常重要的,如果在数据发送完并被确认前时间到,close将返回EWOULDBLOCK错误且套接口发送缓冲区中的任何数据都丢失。close的成功返 回仅告诉我们发送的数据(和FIN)已由对方TCP确认,它并不能告诉我们对方应用进程是否已读了数据。如果套接口设为非阻塞的,它将不等待close完成。
      l_linger的单位依赖于实现,4.4BSD假设其单位是时钟滴答(百分之一秒),但Posix.1g规定单位为秒。

setsockopt()函数用于任意类型、任意状态套接口的设置选项值。尽管在不同协议层上存在选项,但本函数仅定义了最高的“套接口”层次上的选项。选项影响套接口的操作,诸如加急数据是否在普通数据流中接收,广播数据是否可以从套接口发送等等。

  • 有两种套接口的选项:
    • 一种是布尔型选项,允许或禁止一种特性;
    • 另一种是整形或结构选项。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值