如今网络应用随处可见,web、http、email 等这些都是网络应用程序,他们都有着基于相同的基本编程模型,有着相似的整体逻辑结构,并且还有着相同的编程接口。我们需要了解基本的客户端-服务器编程模型。
1.1 客户端-服务器编程模型
每个应用程序都是基于客户端-服务器编程模型的,他们由一个服务器进程和多个客户端进程组成,服务器管理某种资源,通过操作这种资源来为客户端提供某种服务。例如ftp服务器管理磁盘文件,为客户端存储和检索。
客户端-服务器编程模型中的基本操作是事务,一个客户端-服务器事务由以下四步组成
1.2 网络
通常,客户端和服务器是在不同主机上,他们通过计算机网络硬件和软件资源来通信。网络是一个很复杂的系统(过段时间我会做一个计算机网络的专栏来讲解,这里不大体说一下)。对于操作系统来说,网络只是一个
I/O设备,是数据源和数据接收方,一个插到I/O总线扩展槽的适配器提供了网络的物理接口,从网络上接收到的数据从适配器经过I/O和内存总线复制到内存,通常是通过DMA传送,当然数据也可以从内存到网络。
如图:
1.3 两个重要的模型 OSI 和TCP IP 模型
网络将不同功能分为不同模块,以分层的形式组合在一起,每层实现不同的功能,其内部实现方法对外部来说是透明的,每层向上提供服务,同时使用下层服务。
OSI开放系统互联模型
应用层 应用程序:FTP、E-mail、Telnet
表示层 数据格式定义、数据转换/加密
会话层 建立通信进程的逻辑名字与物理名字之间的联系
传输层 差错处理/恢复,流量控制,提供可靠的数据传输
网络层 数据分组、路由选择
数据链路层 数据组成可发送、接收的帧
物理层 传输物理信号、接口、信号形式、速率
TCP/IP协议族:传输控制/网际协议(Transfer Control Protocol/Internet Protocol) 又称作网络通讯协议
TCP/IP协议是Internet事实上的工业标准,
应用层 TFTP,HTTP,SNMP,FTP,SMTP,DNS,Telnet
传输层 TCP,UDP
网络层 IP,ICMP,RIP,OSPF,BGP,IGMP
网络接口与物理层 SLIP,CSLIP,PPP,ARP,RARP,MTU ISO2110,IEEE802.1,EEE802.2
TCP(Transport Control Protocol)传输控制协议
IP(Internetworking Protocol)网间协议
UDP(User Datagram Protocol)用户数据报协议
SMTP(Simple Mail Transfer Protocol)简单邮件传输协议
HTTP(Hypertext Transfer Protocol) 超文本传输协议
FTP(File Transfer Protocol)文件传输协议
ARP(Address Resolution Protocol)地址解析协议
1.4 IP地址
IP地址就是一个32位无符号的整数(IPV4)或者128位无符号的整数(IPV6),是因特网中主机的唯一标识。
ipv4表示形式:点分十进制,192.168.3.178
IP地址分类(依据ipv4前八位来区分)
A类 0000 0000 - 0111 1111 0.0.0.1 - 126.255.255.255
B类 1000 0000 - 1011 1111 128.0.0.1 - 191.255.255.255
C类 1100 0000 - 1101 1111 192.0.0.1 - 223.255.255.255
D类 1110 0000 - 1110 1111 224.0.0.1 - 239.255.255.255 表示组播地址(多投点数据传送)
E类 1111 0000 - 1111 1111 240.0.0.1 - 255.x.x.x 属于保留测试
127ip地址是保留回环地址,使用保留地址的网络只能在内部进行通信,而不能与其他网络互连。
1.5 域名
因特网客户端和服务器相互通信时使用的是IP地址,后来因特网定义了一组人性化的机制,将域名映射到IP地址,域名是一串用局点分割的单纯(字母、数组、破折号)。这个映射通过一个分布
世界范围内的数据库(DNS,域名系统)来管理,其每条定义了一组域名和一组IP地址之间的映射。每台主机都有本地定义的域名俗称 localhost ,这个域名总是映射回送地址127.0.0.1。
一个域名可以和一个ip映射。
多个域名可以映射同一个IP
多个域名可以映射同一组的多个IP
1.6 端口
客户端和服务器通过在连接上发送和接收字节流来通信,从连接一对进程来说是点对点的,从数据同时可以双向流动来说是全双工的,并且发出去的字符和接收到的字符来说是可靠的。
一个套接字连接的是一个端点,每个套接字都有相应的套接字地址,是由一个因特网地址和一个16位整数端口组成的,用 “端口” 来表示。
当客户端发起一个请求时,客服端的端口是由内核自动分配的,称为临时端口,而服务器的端口通常是已分配好的,例如web端口是80,email是25,我们可以在/etc/services来查到。
通俗点讲为了区分一台主机接收到的数据包应该转交给哪个进程来进行处理,使用端口号来区别。
1.7 字节序
字节序是一个处理器架构的特性,用于指示像整数这样的大数据内部的字节如何排序,同一台计算机内进程通信不需要关心这个问题。字节序分为大端序和小端序。
大端序:最大字节地址出现在最低有效字节上
小端序:最小字节地址出现在最低有效字节上
例如一个值:0x04030201,如果用一个指针 p 指向这个值,那么小端 p[0]存储1,p[3]存储4,大端相反。
1.8 套接字
套接字是通信端点的抽象,如同文件描述符一样。用来创建网络应用,
流式套接字(SOCK_STREAM) TCP
提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复的发送且按发送顺序接收。内设置流量控制,避免数据流淹没慢的接收方。
数据被看作是字节流,无长度限制。
数据报套接字(SOCK_DGRAM) UDP
提供无连接服务。数据包以独立数据包的形式被发送,不提供无差错保证,数据可能丢失或重复,顺序发送,可能乱序接收。
原始套接字(SOCK_RAW)
可以对较低层次协议如IP、ICMP直接访问。使用这个套接字需要程序字节构造协议头部,同时需要超级用户权限。
1.9 UDP和TCP
相同点:
同为传输层协议
不同点:
tcp:有连接,保证可靠
udp:无连接,不保证可靠
TCP(即传输控制协议)
是一种面向连接的传输层协议,它能提供高可靠性通信(即数据无误、
数据无丢失、数据无失序、数据无重复到达的通信)
适用情况:
适合于对传输质量要求较高,以及传输大量数据的通信。
在需要可靠数据传输的场合,通常使用TCP协议
MSN/QQ等即时通讯软件的用户登录账户管理相关的功能通常采用TCP协议
应用: SMTP 电子邮件
TELNET 远程终端接入
HTTP 万维网
FTP 文件传输
UDP(User Datagram Protocol)用户数据报协议
是不可靠的无连接的协议。在数据发送前,因为不需要进行连接,所以
可以进行高效率的数据传输。
适用情况:
发送小尺寸数据(如对DNS服务器进行IP地址查询时)
在接收到数据,给出应答较困难的网络中使用UDP。(如:无线网络)
适合于广播/组播式通信中。
MSN/QQ/Skype等即时通讯软件的点对点文本通讯以及音视频通讯通常采用UDP协议
流媒体、VOD、VoIP、IPTV等网络多媒体服务中通常采用UDP方式进行实时数据传输
应用: DNS 域名转换
TFTP 文件传输
SNMP 网络管理
NFS 远程文件服务器
1.10 网络编程流程
TCP:
服务器:
创建套接字 socket( )
填充服务器网络信息结构体 sockaddr_in
将套接字与服务器网络信息结构体绑定 bind( ) 固定自己的信息
将套接字设置为被动监听状态 listen( )
阻塞等待客户端的连接请求 accept( )
进行通信 接收数据recv( )/发送数据 send( )
关闭套接字close()
客户端:
创建套接字 socket( )
填充服务器网络信息结构体 sockaddr_in
发送客户端连接请求 connect( )
进行通信 发送数据 send( )/接收数据recv( )
关闭套接字close()
UDP:
服务器
创建套接字 socket( )
填充服务器网络信息结构体 sockaddr_in
将套接字与服务器网络信息结构体绑定 bind( )
进行通信 recvfrom( )/sendto( )
客户端
创建套接字 socket( )
填充服务器网络信息结构体 sockaddr_in
进行通信 sendto( )/recvfrom( )
2.1 创建套接字
#include /* See NOTES */
#include
int socket(int domain, int type, int protocol);
功能:创建一个套接字
参数:
domain:通信域,协议族
AF_UNIX:本地通信
AF_INET:ipv4网络协议
AF_INET6:ipv6网络协议
AF_PACKET:底层通信协议
type:SOCK_STREAM:流式套接字 tcp(面向连接的套接字)
SOCK_DGRAM:数据报套接字 UDP (无连接的套接字)
SOCK_RAW:原始套接字
SOCK_SEQPACKET:固定长度的、有序的、可靠的、面向连接的报文传递。
protocol:协议,一般为0
如果需要其他协议:
IPPROTO_IP:ipv4网际协议
IPPROTO_IPV6:ipv6网际协议
IPPROTO_ICMP:因特网控制报文协议
IPPROTO_RAW:原始IP数据包协议
IPPROTO_TCP:传输控制协议
IPPROTO_UDP:用户数据报协议
返回值:
成功:文件描述符
失败:-1
注释:对于数据报(SOCK_DGRAM),两个对等进程之间通信时不需要建立逻辑连接,只需要向对等进程的套接字发送报文。
字节流(SOCK_STREAM),在交换数据前需要建立连接。
socket套接字与open相似,使用完之后直接用close关闭即可。
套机制通信是双向的,我们可以使用shutdown函数来禁止一个套接字的I/O。
#include
int shutdown(int sockfd, int how);
参数:sockfd:socket函数返回的fd
how:SHUT_RD:关闭读端:无法从套接字读数据
SHUT_WR:关闭写端:无法向套接字写数据
SHUT_RDWR:关闭读写端:既无法读又无法写。
设置允许重用本地地址和端口(最好加上)
int on = 1;
if (0 > setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)))
{
perror("setsockopt");
return -1;
}
对于无连接的套接字(UDP),数据包到达时可能已经没有了次序,因此如果不能将所有数据放在一个数据包中,则应用程序必须关系数据包的次序,另外无连接的套接字数据包可能会丢失,所以
如果不可以容忍丢失数据就使用面向连接的套接字(tcp)
2.2 字节序转换
一般Linux系统采用小端序,TCP/IP协议采用大端序。使用下面四个函数来处理处理器字节序和网络字节序之间的转换。
#include
uint32_t htonl(uint32_t hostlong);
返回值:以网络字节序表示的32位整数
uint16_t htons(uint16_t hostshort);
返回值:以网络字节序表示的16位整数
uint32_t ntohl(uint32_t netlong);
返回值:以主机字节序表示的32位整数
uint16_t ntohs(uint16_t netshort);
返回值:以主机字节序表示的16位整数
区分:h:主机
n:网络
l:长(4字节)整数
s:短(2字节)整数
#include
#include
#include
int inet_aton(const char *cp, struct in_addr *inp);
功能;j将cp字符串转换成32位网络字节序二进制值存放在inp
返回值:失败 0 成功 非0
in_addr_t inet_addr(const char *cp);
功能:同上,返回转换后的地址
in_addr_t inet_network(const char *cp);
char *inet_ntoa(struct in_addr in);
功能:将32位网络字节序二进制地址转换为字符串
struct in_addr inet_makeaddr(int net, int host);
in_addr_t inet_lnaof(struct in_addr in);
in_addr_t inet_netof(struct in_addr in);
2.3 关联套接字与地址
对于服务端需要给一个接收客户端请求的服务器套接字关联一个地址,客户端应有一种方法来发现连接服务器所需的地址,最简单办法就是服务器保留一个地址并注册在/etc/services中。
#include /* See NOTES */
#include
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:将套接字et与网络信息结构体绑定
参数:
sockfd:文件描述符,socket的返回值
addr:网络信息结构体
通用的(一般不用)
struct sockaddr {
sa_family_t sa_family; 2 bytes
char sa_data[14]; 14 bytes
}
网络信息结构体:sockaddr_in
#include
struct sockaddr_in {
u_short sin_family; // 地址族, AF_INET,2 bytes
u_short sin_port; // 端口,2 bytes htons(9999);
struct in_addr sin_addr;
==>
struct in_addr
{
in_addr_t s_addr; // IPV4地址,4 bytes inet_addr("172.16.6.123");
};
char sin_zero[8]; // 8 bytes unused,作为填充
};
addrlen:addr的长度
返回值:
成功:0
失败:-1
用法:
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(8888);
serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1");
bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
使用限制:
在进程正在运行的计算机上,指定的地址必须有效,不能指定一个其他机器的地址
地址必须和创建的套接字时的地址族所支持的格式相匹配
地址中端口号不得小于1024,
一个套接字端点只能绑定到一个地址
查询绑定到套接字上的地址
#include
int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
如果已经建立连接使用下面函数查询对方地址
int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
2.4 建立连接
如果要处理一个面向连接(tcp)的网络服务,那么在开始数据交换前,需要在请求服务的套接字(客户端)和提供服务的进程套接字(服务端)之间建立连接。
#include /* See NOTES */
#include
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:发送客户端的连接请求
参数:
sockfd:文件描述符,socket的返回值
addr:自己填充的服务器的网络信息结构体,必须与服务器保持一致
addrlen:addr的长度
返回值:
成功:0
失败:-1
当尝试连接服务器时,可能会因为一些原因导致连接失败,如果需要链接成功,要保证连接的计算机必须是开启并且运行的,服务器必须绑定到一个与之向连接的地址上,并且服务器等待队列有空间。
connect还用于无连接网络服务(UDP)
2.5 建立监听
#include /* See NOTES */
#include
int listen(int sockfd, int backlog);
功能:将套接字设置为被动监听的状态
参数:
sockfd:文件描述符,socket的返回值
backlog:允许同时响应客户端连接请求的个数,最大128,一旦满了就拒绝多余请求,
返回值:
成功:0
失败:-1
2.6 建立连接
#include /* See NOTES */
#include
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能:阻塞等待客户端的连接请求
参数:
sockfd:文件描述符,socket的返回值
addr:获取到的客户端的网络信息结构体
addrlen:接收到客户端的addr的长度
当不需要知道客户端地址时, addr 和addrlen 可设置为NULL
返回值:
成功:新的文件描述符(用于通信)
失败:-1
例子:
int acceptfd;
struct sockaddr_in clientaddr;
acceptfd = accept(sockfd, (struct sockaddr *)&clientaddr, &addrlen);
注释:函数返回的文件描述符是套接字描述符,该描述符连接到调用connect的客户端,与原始套接字描述符具有相同的类型和地址族,原始的套接字将继续保持可用状态并接收其他连接请求。
如果没有连接请求accept将会一直阻塞直到请求来。当然服务器可以使用select和poll来等待请求,这样效率更高,我们下面会说。
2.7 数据传输
2.7.1 发送数据
当发送函数成功返回时并不意味着另一端接收到了数据,只是保证数据已经发送到了网络驱动程序。
#include
#include
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
功能:发送数据
参数:
sockfd:文件描述符
服务器:accept的返回值acceptfd
客户端:socket的返回值sockfd
buf:发送的数据
len:buf的长度
flags: 一般为0
MSG_CONFIRM:提供链路层反馈以保持地址映射有效。
MSG_DONTROUTE:勿将数据包路由出本地网络
MSG_DONTWAIT:允许非阻塞操作
MSG_EOR:发送数据后关闭套接字的发送端
MSG_MORE:延迟发送数据包允许写更多数据
MSG_NOSIGNAL:在写无连接的套接字时不产生SIGPIPE信号
MSG_OOB:如果协议支持,发送外带数据,
返回值:
成功:发送的数据的长度
失败:-1
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
功能:与send函数一样,但是该函数可以在无连接套接字上指定一个目标地址,常用语udp
参数:
socket:文件描述符
message:发送的数据
length:数据的长度
flags:标志位,一般为0
dest_addr:目的地址(需要知道给谁发送)
dest_len:addr的长度
返回值:
成功:发送的数据的长度
失败:-1
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
功能:通过带有msghdr结构体里指定多重缓冲区传输数据
struct msghdr {
void *msg_name; /* optional address */
socklen_t msg_namelen; /* size of address */
struct iovec *msg_iov; /* scatter/gather array */
size_t msg_iovlen; /* # elements in msg_iov */
void *msg_control; /* ancillary data, see below */
size_t msg_controllen; /* ancillary data buffer len */
int msg_flags; /* flags on received message */
};
2.7.2 接收数据
#include
#include
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
功能:接收数据
参数:
sockfd:文件描述符
服务器:accept的返回值acceptfd
客户端:socket的返回值sockfd
buf:接收的数据
len:buf的长度
flags: 一般为0阻塞
MSG_CMSG_CLOEXEC:套接字上接收的文件描述符设置执行时关闭的标准
MSG_DONTWAIT:非阻塞
MSG_ERRQUEUE:接收错误信息作为辅助数据
MSG_OOB:获取外带数据
MSG_PEEK:返回数据包内容而不真正取走数据包
MSG_TRUNC:即使数据包被截断,也返回数据包实际长度
MSG_WAITALL:等待直到所有数据可用
返回值:
成功:接收的数据的长度
失败:-1
0:发送端关闭文件描述符
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
功能:接收数据
参数:
sockfd:文件描述符
buf:接收的数据
len:buf 的长度
flags:标志位,一般为0
src_addr:源的地址(接收者的信息,自动填充)
addrlen:addr的长度
返回值:
成功:接收的数据的字节数
失败:-1
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
功能:将接收到的数据放入多个缓冲区
2.8 带外数据
带外数据是一些通信协议所支持的可选功能,与普通数据相比它允许更高优先级的数据传输,tcp支持带外数据,udp不支持。tcp将带外数据成为紧急数据,而且仅支持一个字节的紧急数据。
2.9 TCP demo
服务端
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define N 512
sem_t sem_r;
sem_t sem_w;
char buf[N];
int accept_fd;
int ret;
void *handler_A(void *arg)
{
sem_wait(&sem_r);
while(1) {
memset(&buf,0,sizeof(buf));
ret = recv(accept_fd,buf,sizeof(buf),0);
if(ret < 0) {
perror("fail to read");
break;
}
else if(ret == 0){
printf("write close\n");
break;
}
else {
fputs(buf,stdout);
}
}
sem_post(&sem_r);
pthread_exit((void *)0);
}
void *handler_B(void *arg)
{
sem_wait(&sem_w);
while(1) {
memset(&buf,0,sizeof(buf));
fgets(buf,sizeof(buf),stdin);
if((send(accept_fd,&buf,sizeof(buf),0)) < 0) {
perror("fail to write");
break;
}
}
sem_post(&sem_r);
pthread_exit((void *)0);
}
int main()
{
int socket_fd;
socklen_t addrlen;
struct sockaddr_in socket_addr;
struct sockaddr_in clientaddr;
pthread_t thread_A,thread_B;
addrlen = sizeof(clientaddr);
socket_fd = socket(AF_INET,SOCK_STREAM,0);
if(socket_fd == -1) {
perror("fail to socket");
exit(1);
}
printf("socket............\n");
memset(&socket_addr,0,sizeof(socket_addr));
socket_addr.sin_family = AF_INET;
socket_addr.sin_port = htons(8878);
socket_addr.sin_addr.s_addr = inet_addr("192.168.1.22");
if((bind(socket_fd,(struct sockaddr *)&socket_addr,sizeof(socket_addr))) == -1) {
perror("fail to bind");
exit(1);
}
printf("bind............\n");
if((listen(socket_fd,5)) == -1) {
perror("fail to listen");
exit(1);
}
printf("listen..........\n");
accept_fd = accept(socket_fd,(struct sockaddr*)&clientaddr,&addrlen);
printf("accept:1ip %s port = %hu\n",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port));
if(sem_init(&sem_r,0,1) < 0) {
perror("fail to sem_init");
exit(1);
}
if(sem_init(&sem_w,0,1) < 0) {
perror("fail to sem_init");
exit(1);
}
if(pthread_create(&thread_A,NULL,handler_A,NULL) != 0) {
perror("fail to pthread_create");
exit(1);
}
if(pthread_create(&thread_B,NULL,handler_B,NULL) != 0) {
perror("fail to pthread_create");
exit(1);
}
pthread_join(thread_A,NULL);
pthread_join(thread_B,NULL);
sem_destroy(&sem_r);
sem_destroy(&sem_w);
close(socket_fd);
close(accept_fd);
}
客户端
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define N 512
char buf[N];
sem_t sem_r;
sem_t sem_w;
int socket_fd;
int ret;
void *handler_A(void *arg)
{
sem_wait(&sem_r);
while(1) {
memset(&buf,0,sizeof(buf));
ret = recv(socket_fd,buf,sizeof(buf),0);
if(ret < 0) {
perror("fail to read");
break;
}
else if(ret == 0){
printf("write close\n");
break;
}
else {
fputs(buf,stdout);
}
}
sem_post(&sem_r);
pthread_exit((void *)0);
}
void *handler_B(void *arg)
{
sem_wait(&sem_w);
while(1) {
memset(&buf,0,sizeof(buf));
fgets(buf,sizeof(buf),stdin);
// value = p->data;
if((send(socket_fd,buf,sizeof(buf),0)) < 0) {
perror("fail to write");
break;
}
}
sem_post(&sem_r);
pthread_exit((void *)0);
}
int main()
{
struct sockaddr_in connect_fd;
pthread_t thread_A,thread_B;
socket_fd = socket(AF_INET,SOCK_STREAM,0);
if(socket_fd == -1) {
perror("fail to socket");
exit(1);
}
printf("socket.............\n");
memset(&connect_fd,0,sizeof(connect_fd));
connect_fd.sin_family = AF_INET;
connect_fd.sin_port = htons(8878);
connect_fd.sin_addr.s_addr = inet_addr("192.168.1.22");
if((connect(socket_fd,(struct sockaddr *)&connect_fd,sizeof(connect_fd))) == -1) {
perror("fail to connect");
exit(1);
}
printf("connect............\n");
if(sem_init(&sem_r,0,1) < 0) {
perror("fail to sem_init");
exit(1);
}
if(sem_init(&sem_w,0,1) < 0) {
perror("fail to sem_init");
exit(1);
}
if(pthread_create(&thread_A,NULL,handler_A,NULL) != 0) {
perror("fail to pthread_create");
exit(1);
}
if(pthread_create(&thread_B,NULL,handler_B,NULL) != 0) {
perror("fail to pthread_create");
exit(1);
}
pthread_join(thread_A,NULL);
pthread_join(thread_B,NULL);
sem_destroy(&sem_r);
sem_destroy(&sem_w);
close(socket_fd);
}
2.10 UDP demo
服务端
#include
#include
#include
#include
#include
#include
#include
#include
int main()
{
/*1. 创建套接字*/
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
perror("socket");
return -1;
}
printf("socket..............\n");
/*2. 绑定本机地址和端口*/
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(8888);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (0 > bind(sockfd, (struct sockaddr*)&addr, \
sizeof(addr)))
{
perror("bind");
return -1;
}
printf("bind..............\n");
/*3. 数据接收*/
struct sockaddr_in cliaddr;
socklen_t addrlen = sizeof(cliaddr);
char buf[1024];
int ret;
while (1)
{
ret = recvfrom(sockfd, buf, sizeof(buf), \
0, (struct sockaddr*)&cliaddr, &addrlen);
if (0 > ret)
{
perror("recvfrom");
break;
}
printf("recv: ");
fputs(buf, stdout);
if (0 > sendto(sockfd, buf, sizeof(buf), 0, \
(struct sockaddr*)&cliaddr, addrlen))
{
perror("sendto");
break;
}
}
/*4. 关闭套接字*/
close(sockfd);
return 0;
}
客服端
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, const char *argv[])
{
if (argc < 2)
{
fprintf(stderr, "Usage: %s \n", argv[0]);
return -1;
}
/*1. 创建套接字*/
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
perror("socket");
return -1;
}
printf("socket..............\n");
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(8888);
addr.sin_addr.s_addr = inet_addr(argv[1]);
/*3. 数据接收*/
char buf[1024];
int ret;
while (1)
{
printf("send: ");
fgets(buf, sizeof(buf), stdin);
ret = sendto(sockfd, buf, sizeof(buf), 0, \
(struct sockaddr*)&addr, sizeof(addr));
if (0 > ret)
{
perror("recvfrom");
break;
}
ret = recvfrom(sockfd, buf, sizeof(buf), \
0, NULL, NULL);
if (0 > ret)
{
perror("recvfrom");
break;
}
printf("recv: ");
fputs(buf, stdout);
}
/*4. 关闭套接字*/
close(sockfd);
return 0;
}
3.1 I/O模型
3.2 IO多路复用
为什么使用I/O多路复用?
如果应用程序处理多路输入输出流,采用阻塞模式的话效率将极其的低,如果采用非阻塞,那么将一直轮询,CPU占用率又会非常高,所以使用I/O多路
IO多路复用基本思想
先构造一张有关描述符的表,然后调用一个函数,当这些文件描述符中的一个或多个已准备好进行IO时函数才返回,函数返回时告诉进程已经有描述符就绪,可以进行IO操作。
实现函数select
select函数可以使我们执行I/O多路转接,通过传给select函数的参数可以告诉内核:
a.我们所关心的描述符
b.对于每个描述符我们所关心的条件,是否想从一个给定描述符读/写,是否关心描述符异常
c.愿意等待多长时间
也可以通过返回值得到以下信息
a.已经准备好的文件描述符
b. 对于读、写、异常者三个条件中每一个,哪些已经准备好
然后我们就可以使用read和write函数读写。
#include
#include
#include
int select(int nfds,fd_set *read_fds,fd_set *write_fds,fd_set *except_fds,struct timeval *timeout);
参数: nfds 所有监控文件描述符最大的那一个 +1.(因为文件描述符编号从0开始,所以要加1)
read_fds 所有可读的文件描述符集合。 没有则为NULL
write_fds 所有可写的文件描述符集合。 没有则为NULL
except_fds 处于异常条件的文件描述符 没有则为NULL
timeval: 超时设置。 NULL:一直阻塞,直到有文件描述符就绪或出错
0 :仅仅监测文件描述符集的状态,然后立即返回
非0 :在指定时间内,如果没有事件发生,则超时返回
返回值:当timeval设置为NULL:返回值 -1 表示出错
>0 表示集合中有多少个描述符准备好
当设置timeval非0时: 返回值 -1:表示出错
>0: 表示集合中有多少描述符准备好
=0: 表示时间到了还没有描述符准备好
对于fd_set数据类型有以下四种处理方式 fd:文件描述符、 fdset文件描述符集合
void FD_SET(int fd,fd_set *fdset): 将fd加入到fdest
void FD_CLR(int fd,fd_set *fdest): 将fd从fdest里面清除
void FD_ZERO(fd_set *fdest): 从fdest中清除所有文件描述符
void FD_ISSET(int fd,fd_set *fdest):判断fd是否在fdest集合中
这些接口实现为宏或者函数,调用 FD_ZERO 将fd_set变量的所有位置设置为0,如果要开启描述符集合的某一位,可以调用 FD_SET ,调用FD_CLR 可以清除某一位,FD_ISSET用来检测某一位是否打开。
在申明了一个描述符集合之后,必须使用FD_ZERO将其清零,下面是使用操作:
fd_set reset;
int fd;
FD_ZERO(&reset);
FD_SET(fd, &reset);
FD_ZERO(STDIN_FILENO, &reset);
if (FD_ISSET(fd, &reset)) {}
对于“准备好” 这个词这里说明一下,什么才是准备好,什么是没有准备好,如果对读集(read_fds/write_fds) 中的一个描述符进行read/write操作没有阻塞则认为是准备好,或者对except_fds有一个未决异常条件,则认为准备好。
一个描述符的阻塞并不影响整个select的阻塞。当文件描述符读到文件结尾时候,read返回0.
4. 实现函数poll
poll函数与select函数相似,不同的是,poll不是为每个条件(读、写、异常)构造一个文件描述符,而是构造一个pollfd结构数组,每个数组元素指定一个描述符编号,poll函数可以用于任何类型的文件描述符。
#include
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数:fds:pollfd结构数组
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 请求事件 */
short revents; /* 返回事件 */
};
events:需要将events设置为以下一个或者多个值,这些值会告诉内核哪些是我们关系的文件描述符
POLLIN 不阻塞地读高优先级数据意外的数据
POLLRDNORM 不阻塞地读普通数据
POLLRDBAND 不阻塞地读优先级数据
POLLPRI 不阻塞地读高优先级数据
POLLOUT 普不阻塞地读写普通数据
POLLWRNORM 同POLLOUT
POLLWRBAND 不阻塞地写低优先级数据
POLLERR 发生错误
POLLHUP 发生挂起(当挂起后就不可以再写该描述符,但是可以读)
POLLNVAL 描述字不是一个打开的文件
revents:返回的文件描述符,用于说明描述符发生了哪些事件。
nfds:数组中元素数
timeout:等待时间
= -1:永远等待,直到有一个描述符准备好,或者捕捉到一个信号,如果捕捉到信号返回-1。
= 0 :不等待,立即返回。这是轮询的方法。
> 0: 等待的毫秒数,有文件描述符准备好或者timeout超时立即返回。超时返回值为0.
5. 应用demo
select 程序
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main()
{
int fd = open("/dev/input/mouse1", O_RDONLY);
if (fd < 0)
{
perror("open");
return -1;
}
fd_set fds, rfds;
FD_ZERO(&fds); //清空集合
FD_SET(0, &fds); //把键盘加入集合中
FD_SET(fd, &fds); //把鼠标加入集合中
int retval;
while (1)
{
rfds = fds;
struct timeval tv = {1, 0};
//retval = select(fd+1, &rfds, NULL, NULL, NULL);
retval = select(fd+1, &rfds, NULL, NULL, &tv);
if (retval < 0)
{
perror("select");
break;
}
else if (0 == retval)
{
printf("timeout..........\n");
continue;
}
char buf[1024];
if (FD_ISSET(0, &rfds)) //判断是否是键盘产生事件
{
read(0, buf, sizeof(buf));
printf("Data from Keyboard!\n");
}
if (FD_ISSET(fd, &rfds))
{
read(fd, buf, sizeof(buf));
printf("Data from Mouse!\n");
}
}
close(fd);
}
poll 程序
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main()
{
int fd = open("/dev/input/mouse1", O_RDONLY);
if (fd < 0)
{
perror("open");
return -1;
}
#if 0
fd_set fds, rfds;
FD_ZERO(&fds); //清空集合
FD_SET(0, &fds); //把键盘加入集合中
FD_SET(fd, &fds); //把鼠标加入集合中
#else
struct pollfd fds[2];
fds[0].fd = 0; //键盘
fds[0].events = POLLIN; //请求读
fds[1].fd = fd; //鼠标
fds[1].events = POLLIN; //请求读
#endif
int retval;
while (1)
{
retval = poll(fds, 2, 1000);
if (retval < 0)
{
perror("poll");
break;
}
else if (0 == retval)
{
printf("timeout..........\n");
continue;
}
char buf[1024];
if (fds[0].revents == POLLIN) //判断是否是键盘产生事件
{
read(0, buf, sizeof(buf));
printf("Data from Keyboard!\n");
}
if (fds[1].revents == POLLIN)
{
read(fd, buf, sizeof(buf));
printf("Data from Mouse!\n");
}
}
close(fd);
}
四、服务器模型
在网络中,通常都是一个服务器多个客户端,为了处理每个客户端不同的请求,服务器程序必须有不同的处理程序,所以就有了服务器模型,常用的服务器模型有两种:
循环服务器:同一时刻只可以处理一个客户请求
并发服务器:同一时刻可以处理多个客户请求
4.1. 循环服务器模型
服务器运行后等待客户端连接,当接收到一个客户连接后就开始处理请求,处理完之后断开连接。一次只可以处理一个客户端请求,只有处理完当前客户端请求才可以处理下一个客户端请求,如果
一个客户端一直占有则其他客户端将无法连接,所以一般很少用。
socket(...);
bind(...);
listen(...);
while(1)
{
accept(...);
while(1)
{
recv(...);
process(...);
send(...);
}
close(...);
}
4.2. 并发服务器
并发服务器的设计思想服务器接收客户端连接请求后创建子进程来处理客户端服务,但是过多的客户端连接就会有多个子进程,这也会影响服务器效率。
流程如下:
void handler(int sigo)
{
while (waitpid(-1, NULL, WNOHANG) > 0); //一个SIGCHLD可能对应多个僵尸进程,循环收尸
}
int sockfd = socket(...);
bind(...);
listen(...);
signal(SIGCHLD, handler); //注册SIGCHLD信号,当产生SIGCHLD信号时调用handler函数
while(1) {
int connfd = accept(...);
if (fork() = = 0) {
close(sockfd);
while(1)
{
recv(...);
process(...);
send(...);
}
close(connfd);
exit(...);
}
close(connfd);
}
五、套接字属性设置与获取
套接字机制提供了两个套接字接口来控制套接字行为,一个用来获取一个用来查询,可以设置/获取以下三种选项
通用选项,工作在所有套接字类型上
在套接字层次管理的选项,但是依赖下层协议支持
特定于某种协议的选项,每个协议独有的。
#include /* See NOTES */
#include
int getsockopt(int sockfd, int level, int optname,
void *optval, socklen_t *optlen);
功能:查看当前选项值
参数:同setsockopt参数
返回值:成功0,失败-1
int setsockopt(int sockfd, int level, int optname,
const void *optval, socklen_t optlen);
功能:设置套接字选项
参数: sockfd:套接字
level指定控制套接字的层次.可以取三种值:
1)SOL_SOCKET:通用套接字选项.
2)IPPROTO_IP:IP选项.
3)IPPROTO_TCP:TCP选项.
optname:以下值
SO_DEBUG 如果*optval非0,则启动网络驱动调试功能
SO_REUSEADDR 如果*optval非0,则重用bind中地址
SO_TYPE 标识套接字类型(仅getsockopt)
SO_ERROR 返回挂起套接字错误并清除
SO_DONTROUTE 如果*optval非0,绕过通常路由
SO_BROADCAST 如果*optval非0,广播数据报
SO_SNDBUF 发送缓冲区的字节长度
SO_SNDTIMEO 套接字发送调用超时值
SO_RCVBUF 接收缓冲区的字节长度
SO_RCVTIMEO 套接字接收调用超时值
SO_KEEPALIVE 如果*optval非0,启用周期性keep-alive报文
SO_OOBINLINE 如果*optval非0,将带外数据放在普通数据中
SO_LINGER 当有未发送报文而套接字关闭时,延迟时间
optval:0:禁止选项
1:开启选项
optlen:指向optval参数的大小
返回值:成功0,失败-1
设置超时监测:1.struct timeval tv = {1, 0};
if (0 > setsockopt(connfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)))
{
perror("setsockopt");
return -1;
}
2. struct timeval tv = {5, 0};
select(fd+1, &rfds, NULL, NULL, &tv);
六、广播和组播
上面所说的都属于单播,如果发给局域网中所有主机则为广播,只有UDP协议才可以使用广播。
因为广播可以发送给所有主机,过多广播会占用大量网络宽带,所以组播是将一些主机加入到一个多播组,只有该组的主机可以接收。
6.1 广播发送流程
6.2 广播接收
6.3 组播发送流程
6.4 组播接收流程
5.5 广播demo
发送数据
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, const char *argv[])
{
/*1. 创建数据报套接字*/
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
perror("socket");
return -1;
}
printf("socket..............\n");
/*2. 设置允许发送广播包*/
int on = 1;
if (0 > setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on)))
{
perror("setsockopt");
return -1;
}
/*3. 指定接收方的地址为广播地址,以及端口号*/
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(8888);
addr.sin_addr.s_addr = inet_addr("192.168.3.255");
/*4. 发送数据*/
char buf[1024];
int ret;
while (1)
{
printf("send: ");
fgets(buf, sizeof(buf), stdin);
ret = sendto(sockfd, buf, sizeof(buf), 0, \
(struct sockaddr*)&addr, sizeof(addr));
if (0 > ret)
{
perror("recvfrom");
break;
}
}
/*4. 关闭套接字*/
close(sockfd);
return 0;
}
接收数据
#include
#include
#include
#include
#include
#include
#include
#include
int main()
{
/*1. 创建套接字*/
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
perror("socket");
return -1;
}
printf("socket..............\n");
int on = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
/*2. 绑定广播地址和端口*/
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(8888);
addr.sin_addr.s_addr = inet_addr("192.168.3.255");
if (0 > bind(sockfd, (struct sockaddr*)&addr, \
sizeof(addr)))
{
perror("bind");
return -1;
}
printf("bind..............\n");
/*3. 数据接收*/
struct sockaddr_in cliaddr;
socklen_t addrlen = sizeof(cliaddr);
char buf[1024];
int ret;
while (1)
{
ret = recvfrom(sockfd, buf, sizeof(buf), \
0, (struct sockaddr*)&cliaddr, &addrlen);
if (0 > ret)
{
perror("recvfrom");
break;
}
printf("recv: ");
fputs(buf, stdout);
}
/*4. 关闭套接字*/
close(sockfd);
return 0;
}
六、扩展函数
6.1 获取/设置主机名称
#include
int gethostname(char *name, size_t len);
int sethostname(const char *name, size_t len);
6.2 获取与套接字相连的远程协议地址
#include
int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
6.3 获取本地套接字接口协议地址
#include
int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
6.4 根据主机名获取主机信息
#include
struct hostent *gethostbyname(const char *name);
6.5 根据主机地址获取主机地址
#include /* for AF_INET */
struct hostent *gethostbyaddr(const void *addr, socklen_t len, int type);
6.6 根据主机协议名获取主机协议信息
#include
struct protoent *getprotobyname(const char *name);
6.7 根据协议号获取主机协议信息
#include
struct protoent *getprotobynumber(int proto);