创建socket
int socket(int domain, int type, int protocol);
-
socket()打开一个网络通讯端口,如果成功的话,
返回一个socket文件描述符(有一种文件类型是socket文件)
-
应用程序可以像读写文件一样用read/write在网络上收发数据
-
如果socket()调用失败则返回-1
-
对于IPv4,domain参数指定为AF_INET(或者是PF_INET,两者一样);对于IPv6,domain参数指定为AF_INET6(或者是PF_INET6,两者一样);对于UNIX本地域协议族而言,该参数应该设置为AF_UNIX(或者是PF_UNIX,两者一样)
-
对于TCP协议,type参数指定为SOCK_STREAM,表示面向字节流的传输协议;对于UDP协议,type参数指定为SOCK_DGRAM,表示面向数据报的传输协议
-
需要注意一点!!Linux内核版本2.6.17开始,type参数可以接受上述类型服务与下面两个重要的标志相与的值(&):SOCK_NONBOLCK和SOCK_CLOEXEC,它们分别表示将创建的该socket设为非阻塞的,以及调用fork创建子进程时在子进程中关闭该socket。在内核版本2.6.17之前,socket文件描述符的这两个属性都需要通过额外的系统调用(fcntl)来设置。
-
protocol参数表示在前两个参数构成的协议集合下,再选择一个具体的协议。不过这个值通常都是唯一的(前两个参数已经完全决定了它的值),因此几乎在所有情况下,指定为0即可,表示使用默认协议
命名socket
int bind(int sockfd,const struct sockaddr* addr,socklen_t addrlen);
-
创建socket时,我们给它指定了地址族,但是并未指定使用该地址族中的哪个具体socket地址。讲一个socket文件描述符与socket地址绑定起来的动作叫做socket命名
-
服务器程序所监听的网络地址和端口号通常是固定不变的,因此服务器程序中通常要命名socket,这样客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接。
需要调用bind绑定一个socket地址(socket地址中包含网络地址(IP)和端口号(port))
-
客户端则不用命名socket,而是通常采用匿名方式,即使用操作系统自动分配的socket地址。
-
bind()的作用是将参数sockfd和addr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号
-
struct sockaddr*是一个通用指针类型,addr参数实际上可以接受多种协议的sockaddr结构体,所以这里需要通过指针强转来给它们强转为socket通用地址sockaddr。但它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度(用sizeof(addr)来表示addr的大小)
-
bind()成功返回0,失败返回-1并设置errno
。通常bind()调用失败会设置两者常见的errno:
-
EACCES:被绑定的地址是受保护的地址,仅超级用户可以访问。比如普通用户将socket绑定到知名服务端口上(端口号为0-1023)上时,bind设置此错误
-
EADDRINUSE:被绑定的地址正在使用中。比如将socket绑定到一个处于TIME_WAIT状态的socket地址
-
监听socket
int listen(int sockfd, int backlog);
-
socket被命名之后,还不能马上接受客户端连接,我们需要使用listen函数来创建一个监听队列以存放待处理的客户连接
-
listen系统调用使sockfd处于监听状态
-
backlog参数提示内核监听队列的最大长度(即最多几个客户端处于连接等待状态),如果监听队列的长度超过backlog,服务器接收到的更多的连接请求就被忽略,客户端也将收到ECONNREFUSED错误信息
-
在内核版本2.2之前的Linux之前(这差不多是很老的版本了),backlog参数是指所有处于连接状态(SYN_RCVD)和完全连接状态(ESTABLISHED)的socket上限。但从那之后的内核版本,它表示处于完全连接状态的socket上限,处于半连接状态的socket上限则由/proc/sys/net/ipv4/tcp_max_syn_backlog内核参数定义(我的机器上是128)。
-
这里backlog设置不会太大(典型的backlog值是5)。最终在监听队列中的socket(即ESTABELISHED状态的socket)一般来说会比设置的backlog大一些(如果设置的是5的话,一般监听队列中就是最多有6个完全连接,即backlog+1)
-
listen()成功返回0,失败返回-1
接受连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *len);
-
三次握手完成后,服务器调用accept()从监听队列中接受一个连接
-
如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待,直到有客户端连接上来
-
addr参数用来获取被接受连接的客户端的socket地址,该socket地址的长度由len指出
-
如果给addr的参数传NULL,表示不关心客户端的地址
-
需要注意一下的是,len的类型是socklen_t*,是一个指针,所以传参的时候不能用简单的sizeof(addr),而需要在使用前设置一个socklen_t类型的变量来表示addr的长度
(比如说socklen_t len=sizeof(addr))
,传参的时候传&len
-
len参数是一个传入传出参数,传入的是调用者提供的缓冲区addr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区,即如果创建了一个IPv6的socket地址来接收,实际上接收到的是一个IPv4的socket地址),因为是一个传入传出参数,所以要传地址。
-
accept调用成功返回一个新的socket文件描述符,该socket文件描述符唯一地标识了被接受的这个连接,服务器可通过读写这个新的socket文件描述符来与被接受连接的对应的客户端通信
-
accept只是从监听队列中取出连接,而不论连接处于何种情况(不论是正常的ESTABLISHED状态,还是在调用accept之前,客户端突然主动断开连接,那么此时就是CLOSE_WAIT状态,这样accept也还是会取出这个连接),更不关心网络状况的变化
-
accept的常见使用方式
sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int client_fd = accept(listen_fd,(sockaddr*)&client_addr,&len);
-
首先
创建一个客户端IP(这里是IPv4)
-
接着创建一个socklen_t类型的变量len表示client_addr的大小
-
最后调用accept接收客户端的连接,
取出监听队列中的一个连接,该连接的客户端socket地址被放到client_addr,传给accept的用于接收的len大小是一个缓冲区,如果没有占满缓冲区,返回的就是实际的socket大小(
即如果上面创建的是client_addr是sockaddr_in6的结构,是IPv6的,但是取出的socket地址是IPv4的,这里就会改变这个len的大小,所以这里len要传一个地址,能够改变len指向的缓冲区的大小,而不能简单地采用值传递的方式
)
发起连接
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
-
客户端需要调用connect()连接服务器
-
connect和bind的参数形式一致,区别在于bind的参数是自己的地址,connect的参数是服务器的地址,即addr是服务器的socket地址,addrlen是服务器的地址的大小
-
成功返回0,一旦成功,sockfd就唯一标识这个连接,客户端就可以通过sockfd来与服务器通信
-
connect()调用失败返回-1,并设置以下两个常见的errno
-
ECONNREFUSED:目标端口不存在,连接被拒绝
-
ETIMEOUT:连接超时
-
关闭连接
int close(int fd);
int shutdown(int sockfd,int howto);
-
关闭一个连接实际上就是关闭该连接对应的socket,这可以通过调用关闭普通文件描述符的close()来完成
-
close()系统调用并非总是立即关闭一个连接,而是将fd的引用计数减1。只有当fd的引用计数为0时,才真正关闭连接。
-
多进程程序中,一次fork调用默认将使父进程打开的socket的引用计数加1,因此必须在父子进程都对该socket文件描述符进行close()才能够将连接关闭
-
如果无论如果都要立即关闭连接,而不是将引用计数减1,就可以使用shutdown系统调用,它是专门为网络编程设计的
-
sockfd是要关闭的socket文件描述符,howto决定了shutdown的行为,可以取如下的几个值
-
SHUT_RD:关闭sockfd上读的这一半
。应用程序不能再针对socket文件描述符进行读操作,并且该socket接收缓冲区的数据都将被丢弃
-
SHUT_WR:关闭sockfd上写的这一半
。sockfd的发送缓冲区中的数据会在真正关闭连接之前全部发送出去,应用程序不可以再对该socket进行写操作。这种情况下,
连接处于半关闭状态(即服务器不知道客户端其实已经断开连接的,因为客户端不能够往socket中写任何数据,通知不到服务器自己已经尝试断开连接)
-
SHUT_RDWR:同时关闭sockfd上的读和写
-
-
因此,shutdown能够分别关闭sockfd上的读或写,或者同时关闭;
而close只能将sockfd上的读和写同时关闭
版权声明:本文为lvyibin890原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。