Socket的基本操作函数socket()、bind()、listen()、connect()、accept()、recv()、send()、select()、close()

文章目录

  • Socket的基本操作函数
    • 1、socket()函数
      • 1.1、 命名socket
      • 网络字节序与主机字节序:
    • 2、bind()函数
    • 3、listen()、connect()函数
    • 4、accept()函数
    • 5、recv()、send()等函数
      • 同步Socket的send函数的执行流程
      • 注意:
      • 同步Socket的recv函数的执行流程
      • 注意:
    • 6、select()函数
      • 理解select模型:
      • select模型的特点:
    • 7、close()/shutdown()函数
      • 注:多进程中close操作解释
      • close与shutdown的区别

Socket的基本操作函数socket()、bind()、listen()、connect()、accept()、recv()、send()、select()、close()_第1张图片

Socket的基本操作函数

1、socket()函数

int socket(int domain, int type, int protocol);

socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。

正如可以给fopen的传入不同参数值,以打开不同的文件。创建socket的时候,也可以指定不同的参数创建不同的socket描述符,socket函数的三个参数分别为:

  • domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INET(IPv4)、AF_INET6(IPv6)、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
  • type:指定socket类型。常用的socket类型有,SOCK_STREAM(流式套接字)、SOCK_DGRAM(数据报式套接字)、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等
  • protocol:就是指定协议。常用的协议有,IPPROTO_TCP、PPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。

返回值:

若无错误发生,socket()返回引用新套接口的描述字。否则的话,返回INVALID_SOCKET错误,应用程序可通过WSAGetLastError()获取相应错误代码。

**注意:**并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。

1.1、 命名socket

SOCK_STREAM式套接字的通信双方均需要具有地址,其中服务器端的地址需要明确指定,ipv4的指定方法是使用 struct sockaddr_in类型的变量。

struct sockaddr_in {
     
    sa_family_t    sin_family; /* address family: AF_INET */
    in_port_t      sin_port;   /* port in network byte order */
    struct in_addr sin_addr;   /* internet address */
};
/* Internet address. */
struct in_addr {
     
    uint32_t       s_addr;     /* address in network byte order */
};
struct sockaddr_in     servaddr;  
memset(&servaddr, 0, sizeof(servaddr));  
servaddr.sin_family = AF_INET;  
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//IP地址设置成INADDR_ANY,让系统自动获取本机的IP地址。  
servaddr.sin_port = htons(DEFAULT_PORT);//设置的端口

INADDR_ANY就是指定地址为0.0.0.0的地址,这个地址事实上表示不确定地址,或“所有地址”、“任意地址”。也就是表示本机的所有IP,因为有些机子不止一块网卡,多网卡的情况下,这个就表示所有网卡ip地址的意思。
客户端connect时,不能使用INADDR_ANY选项。必须指明要连接哪个服务器IP。

  • htons将主机的无符号短整形数转换成网络字节顺序
  • htonl将主机的无符号长整形数转换成网络字节顺序

网络字节序与主机字节序:

主机字节序就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。引用标准的Big-Endian和Little-Endian的定义如下:
  a) Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
  b) Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
  
网络字节序:4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。

在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是Big-Endian。谨记对主机字节序不要做任何假定,务必将其转化为网络字节序再赋给socket。

当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。

2、bind()函数

bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

函数的三个参数分别为:

  • sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。
  • addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。
struct sockaddr{
     
    sa_family_t  sin_family;   //地址族(Address Family),也就是地址类型
    char         sa_data[14];  //IP地址和端口号
};

sockaddr 是一种通用的结构体,可以用来保存多种类型的IP地址和端口号。要想给 sa_data 赋值,必须同时指明IP地址和端口号,例如”127.0.0.1:80“,但没有相关函数将这个字符串转换成需要的形式,也就很难给 sockaddr 类型的变量赋值。正是由于通用结构体 sockaddr 使用不便,才针对不同的地址类型定义了不同的结构体。 如ipv6对应的是:


struct sockaddr_in6 {
      
    sa_family_t     sin6_family;   /* AF_INET6 */ 
    in_port_t       sin6_port;     /* port number */ 
    uint32_t        sin6_flowinfo; /* IPv6 flow information */ 
    struct in6_addr sin6_addr;     /* IPv6 address */ 
    uint32_t        sin6_scope_id; /* Scope ID (new in 2.4) */ 
};

struct in6_addr {
      
    unsigned char   s6_addr[16];   /* IPv6 address */ 
};

Unix域对应的是:


#define UNIX_PATH_MAX    108

struct sockaddr_un {
      
    sa_family_t sun_family;               /* AF_UNIX */ 
    char        sun_path[UNIX_PATH_MAX];  /* pathname */ 
};
  • addrlen:对应的是地址的长度。
    通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。

返回值:
如无错误发生,则bind()返回0。否则的话,将返回-1,应用程序可通过WSAGetLastError()获取相应错误代码。

3、listen()、connect()函数

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

int listen(int sockfd, int backlog);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

listen函数的

  • 第一个参数即为要监听的socket描述字,
  • 第二个参数为相应socket可以排队的最大连接个数。

socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。

返回值:
如无错误发生,listen()返回0。否则的话,返回-1,应用程序可通过WSAGetLastError()获取相应错误代码。

connect函数的

  • 第一个参数即为客户端的socket描述字,
  • 第二参数为服务器的socket地址,
  • 第三个参数为socket地址的长度。

客户端通过调用connect函数来建立与TCP服务器的连接。

返回值:
若无错误发生,则connect()返回0。否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。对非阻塞套接口而言,若返回值为SOCKET_ERROR则应用程序调用WSAGetLastError()。如果它指出错误代码为WSAEWOULDBLOCK,则您的应用程序可以:

  1. 用select(),通过检查套接口是否可写,来确定连接请求是否完成。
  2. 如果您的应用程序使用基于消息的WSAAsyncSelect()来表示对连接事件的兴趣,则当连接操作完成后,您会收到一个FD_CONNECT消息。

4、accept()函数

TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就向TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

accept函数的

  • 第一个参数为服务器的socket描述字,
  • 第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址,
  • 第三个参数为客户端协议地址的长度。如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。

返回值:
如果没有错误产生,则accept()返回一个描述所接受包的SOCKET类型的值。否则的话,返回INVALID_SOCKET错误,应用程序可通过调用WSAGetLastError()来获得特定的错误代码。

注意:
accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字
accept函数返回的是已连接的socket描述字。两个套接字不一样。
一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。

5、recv()、send()等函数

至此服务器与客户已经建立好连接了。可以调用网络I/O进行读写操作了,即实现了网咯中不同进程之间的通信!网络I/O操作有下面几组:
• read()/write()
• recv()/send()
• readv()/writev()
• recvmsg()/sendmsg()
• recvfrom()/sendto()

它们的声明如下:

#include 
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
#include 
#include 
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);

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

write函数将buf中的nbytes字节内容写入文件描述符fd。成功时返回写的字节数。失败时返回-1,并设置errno变量。 在网络程序中,当我们向套接字文件描述符写时有两种可能。
1)write的返回值大于0,表示写了部分或者是全部的数据。 2)返回的值小于0,此时出现了错误。我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接)。

recv函数和send函数提供了read和write函数一样的功能,不同的是他们提供了四个参数。前面的三个参数和read、write函数是一样的。

参数

  • 第一个参数指定发送端套接字描述符;
  • 第二个参数指明一个存放应用程序要发送数据的缓冲区;
  • 第三个参数指明实际要发送的数据的字节数;
  • 第四个参数一般置0。或者是以下组合:
    MSG_DONTROUTE:不查找表,是send函数使用的标志,这个标志告诉IP,目的主机在本地网络上,没有必要查找表,这个标志一般用在网络诊断和路由程序里面。
    MSG_OOB:表示可以接收和发送带外数据。
    MSG_PEEK:查看数据,并不从系统缓冲区移走数据。是recv函数使用的标志,表示只是从系统缓冲区中读取内容,而不清楚系统缓冲区的内容。这样在下次读取的时候,依然是一样的内容,一般在有个进程读写数据的时候使用这个标志。
    MSG_WAITALL:等待所有数据,是recv函数的使用标志,表示等到所有的信息到达时才返回,使用这个标志的时候,recv返回一直阻塞,直到指定的条件满足时,或者是发生了错误。

同步Socket的send函数的执行流程

当调用该函数时,

(1)send先比较待发送数据的长度len和套接字s的发送缓冲的长度, 如果len大于s的发送缓冲区的长度,该函数返回SOCKET_ERROR;
(2)如果len小于或者等于s的发送缓冲区的长度,那么send先检查协议s的发送缓冲中的数据是否正在发送,如果是就等待协议把数据发送完,如果协议还没有开始发送s的发送缓冲中的数据或者s的发送缓冲中没有数据,那么send就比较s的发送缓冲区的剩余空间和len
(3)如果len大于剩余空间大小,send就一直等待协议把s的发送缓冲中的数据发送完
(4)如果len小于剩余 空间大小,send就仅仅把buf中的数据copy到剩余空间里(注意并不是send把s的发送缓冲中的数据传到连接的另一端的,而是协议传的,send仅仅是把buf中的数据copy到s的发送缓冲区的剩余空间里)。

如果send函数copy数据成功,就返回实际copy的字节数,如果send在copy数据时出现错误,那么send就返回SOCKET_ERROR;如果send在等待协议传送数据时网络断开的话,那么send函数也返回SOCKET_ERROR。

注意:

  1. send函数把buf中的数据成功copy到s的发送缓冲的剩余空间里后它就返回了,但是此时这些数据并不一定马上被传到连接的另一端。如果协议在后续的传送过程中出现网络错误的话,那么下一个socket函数就会返回SOCKET_ERROR。(每一个除send外的socket函数在执 行的最开始总要先等待套接字的发送缓冲中的数据被协议传送完毕才能继续,如果在等待时出现网络错误,那么该Socket函数就返回 SOCKET_ERROR)
  2. 在Unix系统下,如果send在等待协议传送数据时网络断开的话,调用send的进程会接收到一个SIGPIPE信号,进程对该信号的默认处理是进程终止。

通过测试发现,异步socket的send函数在网络刚刚断开时还能发送返回相应的字节数,同时使用select检测也是可写的,但是过几秒钟之后,再send就会出错了,返回-1。select也不能检测出可写了。


同步Socket的recv函数的执行流程

当应用程序调用recv函数时,

(1)recv先等待s的发送缓冲中的数据被协议传送完毕,如果协议在传送s的发送缓冲中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR,

(2)如果s的发送缓冲中没有数据或者数据被协议成功发送完毕后,recv先检查套接字s的接收缓冲区,如果s接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,直到协议把数据接收完毕。当协议把数据接收完毕,recv函数就把s的接收缓冲中的数据copy到buf中(注意协议接收到的数据可能大于buf的长度,所以 在这种情况下要调用几次recv函数才能把s的接收缓冲中的数据copy完。recv函数仅仅是copy数据,真正的接收数据是协议来完成的),recv函数返回其实际copy的字节数。如果recv在copy时出错,那么它返回SOCKET_ERROR;如果recv函数在等待协议接收数据时网络中断了,那么它返回0。

注意:

在Unix系统下,如果recv函数在等待协议接收数据时网络断开了,那么调用recv的进程会接收到一个SIGPIPE信号,进程对该信号的默认处理是进程终止。

6、select()函数

connect、accept、recv或recvfrom这样的阻塞程序(所谓阻塞方式block,顾名思义,就是进程或是线程执行到这些函数时必须等待某个事件的发生,如果事件没有发生进程或线程就被阻塞,函数不能立即返回)。

可是使用Select就可以完成非阻塞(所谓非阻塞方式non-block,就是进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高)方式工作的程序,它能够监视我们需要监视的文件描述符的变化情况——读写或是异常。


int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval*timeout); 
  • struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符(filedescriptor),即文件句柄,fd_set集合可以通过一些宏由人为来操作。
FD_ZERO(fd_set *set);       //Clear all entries from the set.  
FD_SET(int fd, fd_set *set);    //Add fd to the set.  
FD_CLR(int fd, fd_set *set);    //Remove fd from the set.  
FD_ISSET(int fd, fd_set *set);  //Return true if fd is in the set. 

  • struct timeval代表时间值。
struct timeval {
       
int tv_sec;     //seconds  
int tv_usec;    //microseconds,注意这里是微秒不是毫秒  
}; 

参数:

  • int maxfdp是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1。

  • fd_set * readfds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的读变化的,即我们关心是否可以从这些文件中读取数据了,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读,如果没有可读的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的读变化。

  • fd_set * writefds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的写变化的,即我们关心是否可以向这些文件中写入数据了,如果这个集合中有一个文件可写,select就会返回一个大于0的值,表示有文件可写,如果没有可写的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的写变化。

  • fd_set * errorfds同上面两个参数的意图,用来监视文件错误异常。

  • struct timeval * timeout是select的超时时间,这个参数至关重要,它可以使select处于三种状态,第一,若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;第二,若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;第三,timeout的值大于0,这就是等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。

返回值:
返回状态发生变化的描述符总数。 负值:select错误 ;正值:某些文件可读写或出错 ;0:等待超时,没有可读写或错误的文件

理解select模型:

理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。

(1)执行fd_set set;FD_ZERO(&set);则set用位表示是0000,0000。
(2)若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)
(3)若再加入fd=2,fd=1,则set变为0001,0011
(4)执行select(6,&set,0,NULL,NULL)阻塞等待
(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。

基于上面的讨论,可以轻松得出select模型的特点:

select模型的特点:

(1)可监控的文件描述符个数取决与sizeof(fd_set)的值。我这边服务器上sizeof(fd_set)=512,每bit表示一个文件描述符,则我服务器上支持的最大文件描述符是512*8=4096。据说可调,另有说虽然可调,但调整上限受于编译内核时的变量值。

(2)将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,一是用于再select返回后,array作为源数据和fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始 select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。

(3)可见select模型必须在select前循环array(加fd,取maxfd),select返回后循环array(FD_ISSET判断是否有时间发生)。
使用select和non-blocking实现server处理多client实例
SELECT
Socket的基本操作函数socket()、bind()、listen()、connect()、accept()、recv()、send()、select()、close()_第2张图片

7、close()/shutdown()函数

int close(int sockfd);     

close 一个套接字的默认行为是把套接字标记为已关闭,然后立即返回到调用进程,该套接字描述符不能再由调用进程使用,也就是说它不能再作为read或write的第一个参数,然而TCP将尝试发送已排队等待发送到对端,发送完毕后发生的是正常的TCP连接终止序列。

注:多进程中close操作解释

在多进程并发服务器中,父子进程共享着套接字套接字描述符引用计数记录着共享着的进程个数当父进程或某一子进程close掉套接字时描述符引用计数会相应的减一当引用计数仍大于零时,这个close调用就不会引发TCP的四路握手断连过程。


int shutdown(int sockfd,int howto);  

该函数的行为依赖于howto的值
SHUT_RD:值为0,关闭连接的读这一半。
SHUT_WR:值为1,关闭连接的写这一半。
SHUT_RDWR:值为2,连接的读和写都关闭。
终止网络连接的通用方法是调用close函数。但使用shutdown能更好的控制断连过程(使用第二个参数)。

close与shutdown的区别

主要表现在:

  • close函数会关闭套接字ID,如果有其他的进程共享着这个套接字,那么它仍然是打开的,这个连接仍然可以用来读和写,并且有时候这是非常重要的 ,特别是对于多进程并发服务器来说。
  • shutdown会切断进程共享的套接字的所有连接不管这个套接字的引用计数是否为零,那些试图读得进程将会接收到EOF标识,那些试图写的进程将会检测到SIGPIPE信号,同时可利用shutdown的第二个参数选择断连的方式。

你可能感兴趣的:(#,Windows网络编程,#,Linux系统编程,网络,socket,linux)