网络编程(API)
一.tcp
1.服务器端
1)创建套接字(socket)
int socket(int domain, int type, int protocol);
参数:
domain:协议族,AF_INET
type:通信类型,tcp协议为SOCK_STREAM
protocal:具体的协议,一般为0,默认协议
返回值: 文件描述符
2)将socket返回的文件描述符与本机地址和端口绑定
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
参数:
sockfd:socket返回的文件描述符
addr:地址结构指针,存放ip地址和端口编号
addrlen 地址结构的长度
返回值 0
说明:地址结构体为:
struct sockaddr
{
sa_family_t sa_family; //地址协议族
char sa_data[14]; //地址 端口编号
}
这么写是为了适用于各个协议,却不能将32位的二进制IP地址和短整
型数据的端口编号存储到一个字符数组里,所以使用另一个结构体来
代替它:
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_family); //地址协议族
in_port_t sin_port; //端口编号,2byte
struct in_addr sin_addr; //IPV4地址
};
注:该结构体在/usr/include/netinet/in.h里已被定义,
头文件为<netinet/in.h> ;
注:htons将端口号从主机字节序转化为网络字节序(大端);inet_pton将IP地址按AF_INET协议族转换为网络地址结构,并存入第三个参数中。
3)监听套接字:监听是否有客户端发起连接请求
int listen(int sockfd, int backlog);
参数 sockfd 文件描述符
backlog 等待队列元素个数
当客户端向服务器发起连接时,服务器的监听描述符在端口上收到了
客户端的连 接请求后,并不是立刻建立连接,而是先将请求放在等
待的队列中,然后从队列中按照先后顺序取出请求,建立连接。
返回值 0代表成功
4)接收tcp连接:服务端接收请求并建立连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:sockfd:监听用文件描述符
Addr:入参,是连接来的客户端的地址结构
addrlen 地址结构的长度指针
返回值:文件描述符 用于进行通信的描述符
5)IO:使用read/write,或者recv/send
6)关闭套接字 close(文件描述符)
2.客户端
1) socket:用于通信
2) connect:主动发起连接
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
参数:sockfd 通信用描述符
addr 目标服务器的地址结构指针
addrlen 地址结构的长度
返回值:成功与否 0/非0
3 ) IO
二. udp
1 使用的系统调用
ssize_t sendto(int socket, void *message, size_t length,
int flags, struct sockaddr *dest_addr,
socklen_t dest_len);
ssize_t recvfrom(int socket, void *buffer, size_t length,
int flags, struct sockaddr *address,
socklen_t *address_len);
注意:在没有数据接收时,此函数阻塞
1) 服务端
recvfrom
socket 监听描述符
buffer 接受的数据buf
length buf大小
flags 0
address 当recvfrom接收到数据时,保存客户端地址信息,sendto时使用此结构体
address_len sockaddr结构体大小
sendto
socket 监听描述符
message 发送的数据buf
length 发送数据大小
flags 0
dest_addr 客户端scokaddr_in结构体,recvfrom填充的结构体
dest_len sockaddr 结构体大小
2) 客户端
sendto
socket 描述符
message 发送的数据buf
length 发送数据大小
flags 0
dest_addr 服务端scokaddr_in结构体,填写服务端地址和端口等信息
dest_len sockaddr 结构体大小
recvfrom
socket 描述符
buffer 接受的数据buf
length buf大小
flags 0
address 服务端scokaddr_in结构体,从服务端接受数据 NULL
address_len sockaddr结构体体大小 NULL
问题:服务器与客户端通信时,recv/send、read/write、recvfrom/sendto是阻塞函数,如果一个客户端连接上了又不断开,其他客户端就连不上?解决这个问题需要服务器并发;
三. 并发服务器模型
1.多进程模型:为每一个连接的客户端fock一个子进程来进行I/O,但这会很浪费资源,不建议使用。
代码说明:若有客户端来连接,则为其fork子进程,失败则关闭通信用文件描述符,并重新连接。
代码说明:成功则在子进程进行I/O操作
特点:
① fork是昂贵的。fork时需要复制父进程的所有资源,包括内存映象等;
② fork子进程后,父子进程间、兄弟进程间的通信需要进程间通信IPC机制,给通信带来了困难;
③ 多进程在一定程度上仍然不能有效地利用系统资源;
④ 系统中进程个数也有限制
2. 多线程模型:为每一个连接的客户端创建一个子线程来进行I/O,这不会消耗那么多资源,但如果有共用的全局变量,需要完成多线程的互斥或同步,这会很复杂。
代码说明:通信用描述符需要传参入子线程用于I/O,但这之前可能又有新客户端来连接,iClient就会改变,从而影响子线程,所以在堆区申请空间将iClient放进去来避免这一问题。
代码说明:创建线程,以下为线程函数:
3.I/O多路复用模型:调用函数,创建一个用于监控文件描述符状态的表格,然后轮讯该表格中的所有元素,当某一个描述符有真正的IO请求时,函数返回,判断是哪个文件描述符发生IO请求,就去对该文件描述符进行IO操作。然后将表格恢复成原始状态,继续监控
函数原型:
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
参数:nfds 最大文件描述符+1 为了监控所有文件描述符
readfds 读文件描述符集合地址 该集合中主要保存文件描述符会发生读操作的可能
writefds 写文件描述符集合地址
exceptfds 异常文件描述符集合地址
timeout 时间溢出
NULL:一直阻塞,直到有文件描述符就绪或出错
时间值为0:仅仅检测文件描述符集的状态,然后立即返回
时间值不为0:在指定时间内,如果没有事件发生,则超时返回。
返回值:>0 真正有IO操作的文件描述符的个数
=0 时间溢出但是没有IO
<0 出错
疑问:select并不知道到底是监控中的哪一个描述符状态改变了,该怎么找到那个描述符呢?
void FD_ZERO(fd_set *fdset) 清描述符集合
void FD_SET(int fd,fd_set *fdset) 将描述符增加到集合中
void FD_CLR(int fd,fd_set *fdset) 将描述符从集合中清除
int FD_ISSET(int fd,fd_set *fdset) 判断描述符是否在集合中
注:内核允许的select函数监控的描述符,最多能够监控1024个:
优点:
- 避免了创建多个线程所耗费的资源以及时间。
- 对socket的轮询是内核态的完成,不需要像多线程那样切换需要耗费资源。
而epoll的实现可以做到性能几乎不受连接数(单单是连接而没有其他的操作)的影响。