网络套接字基础2-API接口
服务器地址绑定-bind()
上一节讲到socket用于网络通信,只有套接字绑定一个地址才可以进行进程之间通信.Linux下用bind函数完成一个套接字到地址的绑定.往往是服务器需要这样的绑定.
函数原型:
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
输入参数:
sockfd:即socket描述符,由socket()函数创建唯一标识一个socket,bind()函数就是将给这个描述符绑定一个名字。
addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,如ipv4对应的是16字节:
struct sockaddr_in {
sa_family_t sin_family; /* 16bit address family: AF_INET */
in_port_t sin_port; /* 16bit port in network byte order */
struct in_addr sin_addr; /* 32bit 二进制网络字节序赋值 */
unsigned char sin_zero[8];
};
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
addrlen:对应的是地址的长度
返回值:
成功返回0,失败-1
通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
客户端TCP请求连接-connect()
客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。
函数原型:
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
输入参数:
sockfd,即为客户端的socket描述字,
addr,服务器的socket地址结构.服务器一般只有一个,而客户端则成千上万,所以一般是服务器被动的等待客户端去连接,一般服务器IP地址或者域名是知道的,此时需要点分十进制到二进制地址转换,或者getaddrinfo函数映射得到;
addrlen,为socket地址的长度。
返回值:
成功返回0,失败-1.
客户端通过调用connect函数来建立与TCP服务器的连接。这里编程需要注意连接失败情况,下一次连接应该有一个时间延时,用来保证网络有时间进行自行恢复.
#define MAXSLEEP 128
int connect_retry(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
{
int numsec = 0;
/* 指数补偿算法 */
for(numsec=1; numsec<=MAXSLEEP; numsec<<1)
{
if(connect(sockfd, addr, addrlen) == 0) return 0;
if(numsec<=MAXSLEEP/2) sleep(numsec);
}
return -1;
}
服务器监听客户端请求-listen()
作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。
函数原型:
#include <sys/socket.h>
int listen(int sockfd, int backlog);
输入参数:
sockfd,即为要监听的socket描述字,
backlog为相应socket可以排队等待的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。
返回值:
成功返回0,失败-1.
服务器接受连接请求-accept()
TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就向TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数接受请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。
函数原型:
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //返回连接connect_fd
输入参数:
sockfd,就是上面解释中的监听套接字,这个套接字用来监听一个端口,当客户端连接请求未到时,accept函数会造成服务器程序的阻塞;当有一个客户连接请求时,监听套接字会创建一个新的套接字,该套接字拥有与监听套接字相同的地址和类型,称为连接套接字.它完成与客户端请求的连接,并从accept返回.服务器继续向下执行,开始真正的通信。
addr,这是一个结果参数,存放的客户端地址结构。以为客户端使用connect函数时,客户端地址信息一同发送给服务器了,如果对客户的地址不感兴趣,直接设置为NULL。
addrlen,也是结果的参数,用来接受上述addr的结构的大小的,它指明addr结构所占有的字节个数。同样的,它也可以被设置为NULL。
返回值:
返回连接套接字描述符,这个连接套接字描述符和监听套接字描述符有相同的地址族和类型,只是连接套接字描述符已经连接到调用connect的客户端了,与客户端的通信需要使用此套接字了。如果没有客户端连接请求,accept会一直阻塞住.
一个服务器通常通常仅仅只创建一个监听套接字socket,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接请求创建了一个已连接套接字socket,当服务器完成了对某个客户的服务,相应的已连接套接字socket就被关闭。
自然要问的是:为什么要有两种套接字?原因很简单,如果使用一个描述字的话,那么它的功能太多,使得使用很不直观,同时在内核确实产生了一个这样的新的描述字。连接套接字并没有占用新的端口与客户端通信,依然使用的是与监听套接字一样的端口号
文件读写函数读写套接字-read() write()
一切皆是文件,Linux可以像操作文件一样操作设备.
函数原型:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
read函数是负责从套接字fd中读取内容.当读成功时,read返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了,小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题。
write函数将buf中的nbytes字节内容写入套接字fd.成功时返回写的字节数。失败时返回-1,并设置errno变量。 在网络程序中,当我们向套接字文件描述符写时有俩种可能。1)write的返回值大于0,表示写了部分或者是全部的数据。2)返回的值小于0,此时出现了错误。我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接)。
加入出错处理机制
ssize_t my_read(int fd, void *buf, size_t len)
{
ssize_t done=len;
while(done>0)
{
done = read(fd,buf,len); /* 返回读取字节数 */
if(done != len) /*异常错误*/
{
if(errno == EINTR) done = len;/*信号中断所致,舍弃已读入数据,重新读取*/
else
{
perror("failed read!");
return -1;
}
}
else
break;
}
return done;
}
ssize_t my_write(int fd, void *buf, size_t len)
{
ssize_t done=len;
while(done>0)
{
done = write(fd,buf,len); /* 返回读取字节数 */
if(done != len) /*异常错误*/
{
if(errno == EINTR) done = len;/*信号中断所致,舍弃已读入数据,重新读取*/
else
{
perror("failed to write!");
return -1;
}
}
else
break;
}
return done;
}
面向连接的数据传输-send() recv()
Linux下专门有面向连接的套接字读写函数
#include <sys/types.h>
#include <sys/socket.h>
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);
面向无连接的数据传输-sendto() recvfrom()
由于没有明确的建立连接,所以每次发送一个数据包时都需要明确的指定该数据包的目的地至;在接收数据包时,接受数据包可以得到发送数据包的地址.
#include <sys/types.h>
#include <sys/socket.h>
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);