LV.6 网络编程

D1 网络基础上

1.1网络的历史和分层

Internet的历史

Internet-“冷战”的产物
1957年10月和11月,前苏联先后有两颗“Sputnik”卫星上天
1958年美国总统艾森豪威尔向美国国会提出建立DARPA (Defense Advanced Research Project Agency),即国防部高级研究计划署,简称ARPA
1968年6月DARPA提出“资源共享计算机网络” (Resource Sharing Computer Networks),目的在于让DARPA的所有电脑互连起来,这个网络就叫做ARPAnet,即“阿帕网”,是Interne的最早雏形

网络互联促成了TCP/IP协议的产生

早期的ARPAnet使用网络控制协议(Network Control Protocol,NCP),不能互联不同类型的计算机和不同类型的操作系统,没有纠错功能
1973年由 Robert Kahn 和Vinton Cerf两人合作为ARPAnet开发了新的互联协议。
1974年12月两人正式发表第一份TCP协议详细说明,但此协议在有数据包丢失时不能有效的纠正

TCP协议分成了两个不同的协议:
用来检测网络传输中差错的传输控制协议TCP
专门负责对不同网络进行互联的互联网协议IP

从此,TCP/IP协议诞生

1983年ARPAnet上停止使用NCP,互联网上的主机全部使用TCP/IP协议。TCP/IP协议成为Internet中的“世界语”

网络的体系结构

网络采用分而治之的方法设计,将网络的功能划分为不同的模块,以分层的形式有机组合在一起。

每层实现不同的功能,其内部实现方法对外部其他层次来说是透明1的。每层向上层提供服务,同时使用下层提供的服务

网络体系结构即指网络的层次结构和每层所使用协议的集合

两类非常重要的体系结构:OSI与TCP/IP

OSI开放系统互联模型

OSI模型相关的协议已经很少使用,但模型本身非常通用
OSI模型是一个理想化的模型,尚未有完整的实现

LV.6 网络编程_第1张图片

1.2网络各层协议解释

TCP/IP协议是Internet事实上的工业标准。
LV.6 网络编程_第2张图片

TCP/IP协议通信模型

LV.6 网络编程_第3张图片
各层典型的协议:

  1. 网络接口与物理层
    MAC地址: 48位全球唯一,网络设备的身份标识
    ARP/RARP:
    ARP: IP地址----->MAC地址
    RARP: MAC地址—>IP地址
    PPP协议: 拨号协议(GPRS/3G/4G)
  2. 网络层:
    IP地址
    IP: Internet protocol(分为IPV4和IPV6)
    ICMP: Internet控制管理协议,ping命令属于ICMP
    IGMP: Internet分组管理协议,广播、组播
  3. 传输层:
    TCP: (Transfer Control protocol,传输控制协议) 提供面向连接的,一对一的可靠数据传输的协议
    即数据无误、数据无丢失、数据无失序、数据无重复到达的通信
    UDP: (user Datagram Protocol, 用户数据报协议): 提供不可靠,无连接的尽力传输协议
    是不可靠的无连接的协议。在数据发送前,因为不需要进行连接,所以可以进行高效率的数据传输。
    SCTP: 是可靠传输,是TCP的增强版,它能实现多主机、多链路的通信3456790
  4. 应用层:
    网页访问协议:HTTP2/HTTPS
     邮件发送接收协议: POP3(收)/SMTP(发) 、IMAP(可接收邮件的一部分)
    FTP,
    Telnet/SSH: 远程登录
    嵌入式相关:
    NTP: 网络时钟协议
    SNMP: 简单网络管理协议(实现对网络设备集中式管理)
    RTP/RTSP:用传输音视频的协议(安防监控)

LV.6 网络编程_第4张图片

LV.6 网络编程_第5张图片

1.3网络的封包和拆包

TCP/IP协议下的数据包

LV.6 网络编程_第6张图片
LV.6 网络编程_第7张图片

TCP/IP结构

LV.6 网络编程_第8张图片

TCP协议特点

TCP(即传输控制协议):是一种面向连接的传输层协议,它能提供高可靠性通信(即数据无误、数据无丢失、数据无失序、数据无重复到达的通信)

适用情况:
适合于对传输质量要求较高,以及传输大量数据的通信。
在需要可靠数据传输的场合,通常使用TCP协议
MSN/QQ等即时通讯软件的用户登录账户管理相关的功能通常采用TCP协议

UDP协议的特点

UDP(User Datagram Protocol)用户数据报协议,是不可靠的无连接的协议。在数据发送前,因为不需要进行连接,所以可以进行高效率的数据传输。

适用情况:
发送小尺寸数据(如对DNS服务器进行IP地址查询时)	
在接收到数据,给出应答较困难的网络中使用UDP。(如:无线网络)	
适合于广播/组播式通信中。	
MSN/QQ/Skype等即时通讯软件的点对点文本通讯以及音视频通讯通常采用UDP协议	
流媒体、VOD、VoIP、IPTV等网络多媒体服务中通常采用UDP方式进行实时数据传输

D2 网络基础下

TCP 编程预备知识

SOCKET

  • socket是一个应用编程的接口,

  • 它是一种特殊的文件描述符(对它执行IO的操作函数,比如,read(),write(),close()等操作函数)

  • 并不仅限于TCP/IP协议

  • 面向连接 (Transmission Control Protocol - TCP/IP)

  • 无连接 (User Datagram Protocol -UDP 和 Inter-network Packet Exchange - IPX)

  • socket代表着网络编程的一种资源

  • socket的类型:

    流式套接字(SOCK_STREAM): 唯一对应着TCP
    提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复的发送且按发送顺序接收。内设置流量控制,避免数据流淹没慢的接收方。数据被看作是字节流,无长度限制。
    数据报套接字(SOCK_DGRAM): 唯一对应着UDP
    提供无连接服务。数据包以独立数据包的形式被发送,不提供无差错保证,数据可能丢失或重复,顺序发送,可能乱序接收。
    原始套接字(SOCK_RAW):(对应着多个协议,发送穿透了传输层)
    可以对较低层次协议如IP、ICMP直接访问。

LV.6 网络编程_第9张图片

IP地址

IP地址是Internet中主机的标识

Internet中的主机要与别的机器通信必须具有一个IP地址
IP地址为32位(IPv4)或者128位(IPv6)
每个数据包都必须携带目的IP地址和源IP地址,路由器依靠此信息为数据包选择路由

表示形式:常用点分形式,如202.38.64.10,最后都会转换为一个32位的无符号整数。

 IPV4:采用32位的整数来表示
 IPV6:采用了128位整数来表示

mobileIPV6: local IP(本地注册的IP),roam IP(漫游IP)

IPV4地址:

  点分形式: 192.168.7.246
  32位整数

特殊IP地址:

   局域网IP: 192.XXX.XXX.XXX  10.XXX.XXX.XXX
   广播IP: xxx.xxx.xxx.255, 255.255.255.255(全网广播)
   组播IP: 224.XXX.XXX.XXX~239.xxx.xxx.xxx

端口号

为了区分一台主机接收到的数据包应该转交给哪个进程来进行处理,使用端口号来区别

TCP端口号与UDP端口号独立

端口号一般由IANA (Internet Assigned Numbers Authority) 管理

周知端口:1~1023(1~255之间为常用周知端口,256~1023端口通常由UNIX系统占用)
注册端口:1024~49150
动态或私有端口:49151~65535

16位的数字(1-65535)
众所周知端口: 1~1023(FTP: 21,SSH: 22, HTTP:80, HTTPS:469)
保留端口: 1024-5000(不建议使用)
可以使用的:5000~65535
TCP端口和UDP端口是相互独立的

网络里面的通信是由 IP地址+端口号 来决定

字节序

不同类型CPU的主机中,内存存储多字节整数序列有两种方法,称为主机字节序(HBO):

小端序(little-endian) - 低序字节存储在低地址
将低字节存储在起始地址,称为“Little-Endian”字节序,Intel、AMD等采用的是这种方式;
大端序(big-endian)- 高序字节存储在低地址
将高字节存储在起始地址,称为“Big-Endian”字节序,由ARM、Motorola等所采用

网络中传输的数据必须按网络字节序,即大端字节序

在大部分PC机上,当应用进程将整数送入socket前,需要转化成网络字节序;当应用进程从socket取出整数后,要转化成小端字节序

网络字节序(NBO - Network Byte Order)

  • 使用统一的字节顺序,避免兼容性问题

主机字节序(HBO - Host Byte Order)

  • 不同的机器HBO是不一样的,这与CPU的设计有关
    Motorola 68K系列、ARM系列,HBO与NBO是一致的
    Intel X86系列,HBO与NBO不一致

字节序是指不同的CPU访问内存中的多字节数据时候,存在大小端问题

如CPU访问的是字符串,则不存在大小端问题
大端(Big-Endian):字节的高位在内存中放在存储单元的起始位置
LV.6 网络编程_第10张图片

小端(Little-Endian):与大端相反
LV.6 网络编程_第11张图片
一般来说:

X86/ARM: 小端
powerpc/mips, ARM作为路由器时,大端模式

网络传输的时候采用大端模式

本地字节序、网络字节序
LV.6 网络编程_第12张图片

IP地址转换函数

inet_aton()/inet_addr()

#include
//将strptr所指的字符串转换成32位的网络字节序二进制值
int inet_aton(const char *strptr, struct in_addr *addrptr);

//功能同上,返回转换后的地址。
in_addr_t inet_addr(const char *cp);

cp: 点分形式的IP地址,结果是32位整数(内部包含了字节序的转换,默认是网络字节序的模式)

 特点:
 		1. 仅适应于IPV4
        2. 当出错时,返回-1
        3.此函数不能用于255.255.255.255的转换
inet_pton()/inet_ntop()

#include
int inet_pton(int af, const char *src, void *dst); //将点分十进制的ip地址转化为用于网络传输的数值格式

特点: 
	   		1.适应于IPV4和IPV6
	        2.能正确的处理255.255.255.255的转换问题
	     参数:
			1.af: 地址协议族(AF_INET或AF_INET6)
			2.src:是一个指针(填写点分形式的IP地址[主要指IPV4])
			3.dst: 转换的结果给到dst
			
	RETURN VALUE
   inet_pton() returns 1 on success (network address was successfully con‐
   verted).  0 is returned if src  does  not contain  a  character  string representing a valid network address in the specified address family.  If af does not contain a valid address family, -1 is returned and errno is set to EAFNOSUPPORT
    返回值:若成功则为1,若输入不是有效的表达式则为0,若出错则为-1

const char * inet_ntop(int family, const void *addrptr, char *strptr, size_t len); //将数值格式转化为点分十进制的ip地址格式
返回值:若成功则为指向结构的指针,若出错则为NULL

D3 TCP编程

tcp网络编程流程

服务器端:server.c
创建套接字 socket( )
填充服务器网络信息结构体 sockaddr_in
将套接字与服务器网络信息结构体绑定 bind( )
将套接字设置为被动监听状态 listen( )
阻塞等待客户端的连接请求 accept( )
进行通信 recv( )/send( )
客户端:client.c
创建套接字 socket( )
填充服务器网络信息结构体 sockaddr_in
发送客户端的连接请求 connect( )
进行通信 send( )/recv( )

连接测试:nc

相关函数

网络编程常用函数

socket() 创建套接字
bind() 绑定本机地址和端口
connect() 建立连接
listen() 设置监听端口
accept() 接受TCP连接
recv(), read(), recvfrom() 数据接收
send(), write(), sendto() 数据发送
close(), shutdown() 关闭套接字

socket()函数

LV.6 网络编程_第13张图片
1.1 参数:
1.domain:
AF_INET IPv4 Internet protocols ip(7)
AF_INET6 IPv6 Internet protocols ipv6(7)
AF_UNIX, AF_LOCAL Local communication unix(7)
AF_NETLINK Kernel user interface device netlink(7)
AF_PACKET Low level packet interface packet(7)
PF_NS // Xerox NS协议
PF_IMPLINK // Interface Message协议

2.type:
SOCK_STREAM: 流式套接字 唯一对应于TCP
SOCK_DGRAM: 数据报套接字,唯一对应着UDP
SOCK_RAW: 原始套接字
3.protocol: 一般填0,原始套接字编程时需填充

1.2 返回值:
RETURN VALUE
On success, a file descriptor for the new socket is returned. On
error, -1 is returned, and errno is set appropriately.
成功时返回文件描述符,出错时返回为-1

bind()函数

LV.6 网络编程_第14张图片
2.1 参数:
sockfd: 通过socket()函数拿到的fd
addr: struct sockaddr的结构体变量的地址
addrlen: 地址长度

LV.6 网络编程_第15张图片
RETURN VALUE
On success, zero is returned. On error, -1 is returned, and errno is set appropriately.

通用地址结构

  struct sockaddr
  {    
       u_short  sa_family;    // 地址族, AF_xxx
       char  sa_data[14];     // 14字节协议地址
  };	
  
//Internet协议地址结构
  struct sockaddr_in
  {           
       u_short sin_family;      // 地址族, AF_INET,2 bytes
       u_short sin_port;      // 端口,2 bytes
       struct in_addr sin_addr;  // IPV4地址,4 bytes 	
       char sin_zero[8];        // 8 bytes unused,作为填充
  }; 
  
IPv4地址结构
// internet address  
struct in_addr
{
     in_addr_t  s_addr;            // u32 network address 
};

示例代码:
LV.6 网络编程_第16张图片

如果是IPV6的编程,要使用struct sockddr_in6结构体(详细情况请参考man 7 ipv6),通常更通用的方法可以通过struct sockaddr_storage来编程

地址结构的一般用法

1.定义一个struct sockaddr_in类型的变量并清空
struct sockaddr_in myaddr;
memset(&myaddr, 0, sizeof(myaddr));

2.填充地址信息
myaddr.sin_family = AF_INET;
myaddr.sin_port = htons(8888);
myaddr.sin_addr.s_addr = inet_addr(“192.168.1.100”);

3.将该变量强制转换为struct sockaddr类型在函数中使用
bind(listenfd, (struct sockaddr*)(&myaddr), sizeof(myaddr));

地址转换函数

unsigned long inet_addr(char *address);
address是以’\0’结尾的点分IPv4字符串。该函数返回32位的地址。如果字符串包含的不是合法的IP地址,则函数返回-1。例如:
struct in_addr addr;
addr.s_addr = inet_addr(" 192.168.1.100 ");

char* inet_ntoa(struct in_addr address);
将32位网络字节序二进制地址转换成点分十进制的字符串,address是IPv4地址结构,函数返回一指向包含点分IP地址的静态存储区字符指针。如果错误则函数返回NULL

listen

int listen (int sockfd, int backlog);
sockfd:监听连接的套接字
backlog
指定了正在等待连接的最大队列长度,它的作用在于处理可能同时出现的几个连接请求。同时允许几路客户端和服务器进行正在连接的过程(正在三次握手),一般填5, 测试得知,ARM最大为8。内核中服务器的套接字fd会维护2个链表:

  1. 正在三次握手的的客户端链表(数量=2*backlog+1)
  2. 已经建立好连接的客户端链表(已经完成3次握手分配好了newfd)
    DoS(拒绝服务)攻击即利用了这个原理,非法的连接占用了全部的连接数,造成正常的连接请求被拒绝。
    返回值: 0 或 -1
    RETURN VALUE
    On success, zero is returned. On error, -1 is returned, and errno is set appropriately.

完成listen()调用后,socket变成了监听socket(listening socket)

accept()

#include
#include

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

sockfd : 监听套接字 ,经过前面socket()创建并通过bind(),listen()设置过的fd头文件
addr : 对方地址,获取连接过来的客户的信息
addrlen:地址长度,获取连接过来的客户的信息

返回值:已建立好连接的套接字或-1,RETURN VALUE
On success, these system calls return a nonnegative integer that is a descriptor for the accepted socket. On
error, -1 is returned, and errno is set appropriately.
成功时返回已经建立好连接的新的newfd

listen()和accept()是TCP服务器端使用的函数

connect()

connect()函数和服务器的bind()函数写法类似,connect()是客户端使用的系统调用。

#include
#include
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);

sockfd : socket返回的文件描述符,即通过socket()函数拿到的fd
serv_addr : 服务器端的地址信息 ,struct sockaddr的结构体变量的地址
addrlen : serv_addr的长度

返回值:0 或 -1
RETURN VALUE
If the connection or binding succeeds, zero is returned. On error, -1
is returned, and errno is set appropriately

recv() 接收网络数据

#include
ssize_t recv(int socket, const void *buffer, size_t length, int flags);

buffer : 发送缓冲区首地址
length : 发送的字节数
flags : 接收方式(通常为0)

flags: 
       一般填写0,此时和read()作用一样
    	特殊的标志:
       MSG_DONTWAIT: Enables  nonblocking  operation; 非阻塞版本
       MSG_OOB:用于发送TCP类型的带外数据(out-of-band)
       MSG_PEEK:
              This flag causes the receive operation to return data  from
              the  beginning  of  the receive queue without removing that
              data from the queue.  Thus, a subsequent receive call  will
              return the same data.

返回值:
成功:实际接收的字节数
失败:-1, 并设置errno

send() 发送网络数据

#include
ssize_t send(int socket, const void *buffer, size_t length, int flags);

buffer : 发送缓冲区首地址
length : 发送的字节数
flags : 发送方式(通常为0)

send()比write多一个参数flags:
    flags: 
       一般填写0,此时和write()作用一样
       特殊的标志:
       MSG_DONTWAIT: Enables  nonblocking  operation; 非阻塞版本
       MSG_OOB:用于发送TCP类型的带外数据(out-of-band)

返回值:
成功:实际发送的字节数
失败:-1, 并设置errno

read()/write()

ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);

read()和write()经常会代替recv()和send(),通常情况下,看程序员的偏好,使用read()/write()和recv()/send()时最好统一

close() 套接字的关闭

int close(int sockfd);
关闭双向通讯

int shutdown(int sockfd, int howto);
TCP连接是双向的(是可读写的),当我们使用close时,会把读写通道都关闭,有时侯我们希望只关闭一个方向,这个时候我们可以使用shutdown。

针对不同的howto,系统会采取不同的关闭方式。
shutdown()的howto参数
howto = 0
关闭读通道,但是可以继续往套接字写数据。
howto = 1
和上面相反,关闭写通道。只能从套接字读取数据。 
howto = 2
关闭读写通道,和close()一样

代码

头文件

#ifndef _MAKEU_NET_H_
#define _MAKEU_NET_H_

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define SERV_PORT 5001
#define SERV_IP_ADDR "192.168.3.59"
#define BACKLOG 5    

#define QUIT_STR "quit"

#endif

服务端

#include "net.h" 

int main(int argc, char *argv[])
{
	int fd = -1;
	struct sockaddr_in sin;
	//1.创建socket fd
	if((fd = socket(AF_INET,SOCK_STREAM,0))<0){
		perror("socket");
		exit(1);
	}
	//2.绑定
	//2.1填充struct sockaddr_in结构体变量
	bzero(&sin,sizeof(sin));//置零结构体变量
	sin.sin_family = AF_INET;
	sin.sin_port = htons(SERV_PORT);//网络字节序的端口号
#if 0 
	sin.sin_addr.s_addrs =inet_addr(SERV_IP_ADDR);  
#else
	if(inet_pton(AF_INET,SERV_IP_ADDR,(void*)&sin.sin_addr.s_addr) != 1){
		perror("inet_pton");
		exit(1);
	}
#endif
	//2.2绑定
	if(	bind(fd,(struct sockaddr *)&sin,sizeof(sin))<0){
		perror("bind");
		exit(1);
	}
	//3.调用listen()把主动套接字变成被动套接字
	if(listen(fd,BACKLOG)<0){
		perror("listen");
		exit(1);
	}
	int newfd = -1;
	//4.阻塞等待客户端连接请求
	newfd = accept(fd,NULL,NULL);
	if(newfd <0){
		perror("accept");
		exit(1);
	}



	//5. 读写
	int ret = -1;
	char buf[BUFSIZ];
	while(1){
		bzero(buf,BUFSIZ);
		do{
			ret = read(newfd,buf,BUFSIZ-1);
		}while(ret <0 && EINTR == errno);//EINTR(中断),只有在ret < 0 并且读中断的时候,重新读,其他情况跳出循环
		if(ret <0 ){
			perror("read");
			exit(1);
		}
		if(!ret){//ret > 0 ,对方已关网
			break;
		}
		printf("Receive data: %s\n",buf);
		if(!strncasecmp(buf,QUIT_STR,strlen(QUIT_STR)) ){//客户端输入"quit"退出。QUIT_STR == quit
			printf("Client is exiting!\n");
			break;
		}
			
	}

	close(newfd);
	close(fd);

	//。。。FIXME!!!
	
	return 0;
}

客户端

#include "net.h" 

int main(int argc, char *argv[])
{
	int fd = -1;
	struct sockaddr_in sin;
	//1.创建socket fd
	if((fd = socket(AF_INET,SOCK_STREAM,0))<0){
		perror("socket");
		exit(1);
	}
	//2.连接服务器
	//2.1填充struct sockaddr_in结构体变量
	bzero(&sin,sizeof(sin));//置零结构体变量
	sin.sin_family = AF_INET;
	sin.sin_port = htons(SERV_PORT);//网络字节序的端口号
#if 0 
	sin.sin_addr.s_addrs = inet_addr(SERV_IP_ADDR);
#else
	if(inet_pton(AF_INET,SERV_IP_ADDR,(void*)&sin.sin_addr.s_addr) != 1){
		perror("inet_pton");
		exit(1);
	}
#endif
	//2.2连接
	if(	connect(fd,(struct sockaddr *)&sin,sizeof(sin))<0){
		perror("connect");
		exit(1);
	}
	//3. 写
	char buf[BUFSIZ];
	while(1){
		bzero(buf,BUFSIZ);
		if(fgets(buf,BUFSIZ-1,stdin)==NULL){
			continue;
		}
		write(fd,buf,strlen(buf));
		if(!strncasecmp(buf,QUIT_STR,strlen(QUIT_STR))){
			printf("Client is exiting!\n");
			break;
		}
	}


	//4.关闭连接
	close(fd);

	//。。。FIXME!!!
	
	return 0;
}

D4 并发服务器

TCP/IP网络编程

并发服务器可以同时向所有发起请求的服务器提供服务,大大降低了客户端整体等待服务器传输信息的时间,同时,由于网络程序中,数据通信时间比CPU运算时间长,采用并发服务器可以大大提高CPU的利用率。

TCP编程API

LV.6 网络编程_第17张图片
实现并发服务器有三种方法:
多进程服务器(通过创建多个进程提供服务)
多路复用服务器(通过捆绑并统一管理I/O对象提供服务)
多线程服务器(通过生成与客户端等量的线程提供服务)
LV.6 网络编程_第18张图片

多线程代码

头文件

#ifndef _MAKEU_NET_H_
#define _MAKEU_NET_H_

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define SERV_PORT 5001
#define SERV_IP_ADDR "192.168.3.63"
#define BACKLOG 5    

#define QUIT_STR "quit"

#endif

服务端

#include 
#include "net.h" 
void cli_data_handle(void *arg);

int main(int argc, char *argv[])
{
	int fd = -1;
	struct sockaddr_in sin;
	//1.创建socket fd
	if((fd = socket(AF_INET,SOCK_STREAM,0))<0){
		perror("socket");
		exit(1);
	}
	//优化4:允许绑定地址快速重用
	int b_reuse = 1;
	setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&b_reuse,sizeof(int));


	//2.绑定
	//2.1填充struct sockaddr_in结构体变量
	bzero(&sin,sizeof(sin));//置零结构体变量
	sin.sin_family = AF_INET;
	sin.sin_port = htons(SERV_PORT);//网络字节序的端口号
#if 1
	sin.sin_addr.s_addr =htonl(INADDR_ANY);  
#else
	if(inet_pton(AF_INET,SERV_IP_ADDR,(void*)&sin.sin_addr.s_addr) != 1){
		perror("inet_pton");
		exit(1);
	}
#endif
	//2.2绑定
	if(	bind(fd,(struct sockaddr *)&sin,sizeof(sin))<0){
		perror("bind");
		exit(1);
	}
	//3.调用listen()把主动套接字变成被动套接字
	if(listen(fd,BACKLOG)<0){
		perror("listen");
		exit(1);
	}
	int newfd = -1;
	//4.阻塞等待客户端连接请求
	//用多进程/多线程处理已经建立好连接的客户端数据
	pthread_t tid;

	struct sockaddr_in cin;
	socklen_t addrlen = sizeof(cin);
	while(1){
		if((newfd = accept(fd,(struct sockaddr*)&cin,&addrlen))<0){
			perror("accept");
			exit(1);
		}

		char ipv4_addr[16];
		if(!inet_ntop(AF_INET,(void*)&cin.sin_addr,ipv4_addr,sizeof(cin))){
			perror("inet_ntop");
			exit(1);
		}

		printf("Clinet(%s:%d) is connected!\n",ipv4_addr,ntohs(cin.sin_port));
		pthread_create(&tid,NULL,(void*)cli_data_handle,(void*)&newfd);
	}

	close(fd);
	return 0;
}

void cli_data_handle(void *arg){
	int newfd = *(int*)arg;
	printf("handler thread:newfd = %d\n",newfd);
	//读写
	int ret = -1;
	char buf[BUFSIZ];
	while(1){
		bzero(buf,BUFSIZ);
		do{
			ret = read(newfd,buf,BUFSIZ-1);
		}while(ret <0 && EINTR == errno);
		if(ret <0 ){
			perror("read");
			exit(1);
		}
		if(!ret){//对方已关网
			break;
		}
		printf("Receive data: %s\n",buf);
		if(!strncasecmp(buf,QUIT_STR,strlen(QUIT_STR)) ){
			printf("Client(fd=%d) is exiting!\n",newfd);
			break;
		}
			
	}

	close(newfd);
	
}

客户端

#include "net.h" 

void usage (char *s)
{
	printf ("\n%s serv_ip serv_port",s);
	printf ("\n\t serv_ip: server ip address");
	printf ("\n\t serv_port: server port(>5000)\n\n");
}
int main(int argc, char **argv)
{
	int fd = -1;
	int port = -1;
	struct sockaddr_in sin;
	
	if(argc != 3){
		usage(argv[0]);
		exit(1);
	}



	//1.创建socket fd
	if((fd = socket(AF_INET,SOCK_STREAM,0))<0){
		perror("socket");
		exit(1);
	}
	port =atoi(argv[2]);

	if(port < 5000){
		usage(argv[0]);
		exit(1);
	}




	//2.连接服务器
	//2.1填充struct sockaddr_in结构体变量
	bzero(&sin,sizeof(sin));//置零结构体变量
	sin.sin_family = AF_INET;
	sin.sin_port = htons(port);//网络字节序的端口号
#if 0 
	sin.sin_addr.s_addrs = inet_addr(SERV_IP_ADDR);
#else
	if(inet_pton(AF_INET,argv[1],(void*)&sin.sin_addr.s_addr) != 1){
		perror("inet_pton");
		exit(1);
	}
#endif
	//2.2连接
	if(	connect(fd,(struct sockaddr *)&sin,sizeof(sin))<0){
		perror("connect");
		exit(1);
	}
	//3. 写
	char buf[BUFSIZ];
	int ret = -1;
	while(1){
		bzero(buf,BUFSIZ);
		if(fgets(buf,BUFSIZ-1,stdin)==NULL){
			continue;
		}
		do{
			ret = write(fd,buf,strlen(buf));
		}while(ret <0 && EINTR == errno);

		if(!strncasecmp(buf,QUIT_STR,strlen(QUIT_STR))){
			printf("Client is exiting!\n");
			break;
		}
	}


	//4.关闭连接
	close(fd);

	//。。。FIXME!!!
	
	return 0;
}

Makefile

# Makefile
#

#CROSS_COMPILE = arm-linux-gnu-

CC = $(CROSS_COMPILE)gcc

ifdef CROSS_COMPILE
TARGET = /opt/filesystem
endif


#DEBUG = -g -O0 -Wall
DEBUG = -g -O2
CFLAGS += $(DEBUG)

PROGS = ${patsubst %.c, %, ${wildcard *.c}} 

all : $(PROGS)

install: $(PROGS)
ifdef CROSS_COMPILE
	mkdir $(TARGET)/root/long_term/io -p
	cp $(PROGS) $(TARGET)/root/long_term/io -f
endif
% : %.c
	$(CC)  $(CFLAGS)  $< -o $@ -lpthread

.PHONY: uninstall clean dist

uninstall :
ifdef CROSS_COMPILE
	cd $(TARGET)/root/long_term/io && rm -f $(PROGS)
endif

clean : uninstall
	- rm -f $(PROGS) core *.gz

dist: clean
	tar czf ../../farsight_network_1st_v1.1_for_1507.tar.gz ../../networks
	

D5 UDP编程

udp网络编程流程

服务器:
创建套接字 socket( )
填充服务器网络信息结构体 sockaddr_in
将套接字与服务器网络信息结构体绑定 bind( )
进行通信 recvfrom( )/sendto( )
客户端:
创建套接字 socket( )
填充服务器网络信息结构体 sockaddr_in
进行通信 sendto( )/recvfrom( )

相关函数

sendto(),recvfrom()

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);
LV.6 网络编程_第19张图片
这两个函数一般在使用UDP协议时使用

UDP编程API

LV.6 网络编程_第20张图片
LV.6 网络编程_第21张图片

代码

头文件

#ifndef __MAKEU_NET_H__
#define __MAKEU_NET_H__

#include 
#include 
#include 
#include 
#include 
#include 
#include                   /* See NOTES */
#include 
#include 
#include                  /* superset of previous */
#define SERV_PORT 5001
#define SERV_IP_ADDR "192.168.7.246"
#define QUIT_STR "quit"
#endif

服务端

#include "net.h"

int main(void)
{

	int fd = -1;
 	struct sockaddr_in sin;
        
	/* 1. 创建socket fd */
        if ((fd = socket (AF_INET, SOCK_DGRAM, 0)) < 0) { //udp程序
                perror ("socket");
                exit (1);
        }

	/* 2. 允许绑定地址快速重用 */
        int b_reuse = 1;
        setsockopt (fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int));

	        /*2. 绑定 */
        /*2.1 填充struct sockaddr_in结构体变量 */
        bzero (&sin, sizeof (sin));
        sin.sin_family = AF_INET;
        sin.sin_port = htons (SERV_PORT);       //网络字节序的端口号

        /* 让服务器程序能绑定在任意的IP上 */
#if 1
        sin.sin_addr.s_addr = htonl (INADDR_ANY);
#else
        if (inet_pton (AF_INET, SERV_IP_ADDR, (void *) &sin.sin_addr) != 1) {
                perror ("inet_pton");
                exit (1);
        }
#endif
        /*2.2 绑定 */
        if (bind (fd, (struct sockaddr *) &sin, sizeof (sin)) < 0) {
                perror ("bind");
                exit (1);
        }
	
	char buf[BUFSIZ];
	struct sockaddr_in cin;
	socklen_t addrlen = sizeof(cin);
	printf("\nUDP server started!\n");
	while(1) {
		bzero(buf, BUFSIZ);
		if( recvfrom(fd, buf, BUFSIZ-1, 0,(struct sockaddr *)&cin, &addrlen ) < 0) {
			perror("recvfrom");
			continue;
		}
		
		char ipv4_addr[16];
        if (!inet_ntop (AF_INET, (void *) &cin.sin_addr, ipv4_addr, sizeof (cin))) {
                             perror ("inet_ntop");
                             exit (1);
        }

		printf("Recived from(%s:%d), data:%s",ipv4_addr, ntohs(cin.sin_port), buf);
		
		if (!strncasecmp (buf, QUIT_STR, strlen (QUIT_STR))) {  //用户输入了quit字符
                       printf ("Client(%s:%d) is exiting!\n", ipv4_addr, ntohs(cin.sin_port));
        }

	}

	close(fd);

	return 0;
}


客户端

/*udp demo */
/* usage:
 * ./client serv_ip serv_port 
*/
#include "net.h"
void usage(char *s)
{
	printf("\nThis is udp demo!\n");
	printf("\nUsage:\n\t %s serv_ip serv_port",s);
	printf("\n\t serv_ip: udp server ip address");
	printf("\n\t serv_port: udp server port(serv_port > 5000)\n\n");
}

int main(int argc, char *argv[])
{
	int fd = -1;
	int port = SERV_PORT;
	
	port = atoi(argv[2]);
	if(port < 0 || (port >0 && port <= 5000)) {
		usage(argv[0]);
		exit(1);
	}
        struct sockaddr_in sin;
	if(argc !=3) {
		usage(argv[0]);
		exit(1);
	}        

	/* 1. 创建socket fd*/
        if( (fd = socket(AF_INET,SOCK_DGRAM, 0)) < 0) { //UDP编程
                perror("socket");
                exit(1);
        }

	/*2.1 填充struct sockaddr_in结构体变量 */
        bzero(&sin,sizeof(sin));

        sin.sin_family = AF_INET;
        sin.sin_port = htons(SERV_PORT); //网络字节序的端口号
#if 0
        sin.sin_addr.s_addr = inet_addr(argv[1]);
#else
        if( inet_pton(AF_INET, argv[1], (void *)&sin.sin_addr) != 1) {
                perror("inet_pton");
                exit(1);
        }
#endif	
	printf("UDP client started!\n");
	char buf[BUFSIZ];
	while(1) {
		fprintf(stderr,"pls input string:");
		bzero(buf, BUFSIZ);
		if( fgets(buf, BUFSIZ-1, stdin) ==NULL) {
			perror("fgets");
			continue;
		}
		
		sendto(fd, buf, strlen(buf), 0, (struct sockaddr *)&sin, sizeof(sin)); 
		
		if( !strncasecmp(buf, QUIT_STR, strlen(QUIT_STR))) {  //用户输入了quit字符
                        printf("Client is exited!\n");
                        break;
                }
	
	}
	close(fd);
	return 0;
}

Makefile

# Makefile
#

#CROSS_COMPILE = arm-linux-gnu-

CC = $(CROSS_COMPILE)gcc

ifdef CROSS_COMPILE
TARGET = /opt/filesystem
endif


#DEBUG = -g -O0 -Wall
DEBUG = -g -O2
CFLAGS += $(DEBUG)

PROGS = ${patsubst %.c, %, ${wildcard *.c}} 

all : $(PROGS)

install: $(PROGS)
ifdef CROSS_COMPILE
	mkdir $(TARGET)/root/long_term/io -p
	cp $(PROGS) $(TARGET)/root/long_term/io -f
endif
%.o : %.c
	$(CC)  $(CFLAGS) -c $< -o $@

.PHONY: uninstall clean dist

uninstall :
ifdef CROSS_COMPILE
	cd $(TARGET)/root/long_term/io && rm -f $(PROGS)
endif

clean : uninstall
	- rm -f $(PROGS) core *.gz

dist: clean
	tar czf ../../farsight_network_1st_v1.1_for_1507.tar.gz ../../networks
	

D6 IO多路复用

6.1 IO模型

在UNIX/Linux下主要有4种I/O 模型:
阻塞I/O:最常用
非阻塞I/O:可防止进程阻塞在I/O操作上,需要轮询
I/O 多路复用:允许同时对多个I/O进行控制
信号驱动I/O:一种异步通信模型

6.2 阻塞I/O 模式

阻塞I/O 模式是最普遍使用的I/O 模式,大部分程序使用的都是阻塞模式的I/O 。
缺省情况下,套接字建立后所处于的模式就是阻塞I/O 模式。
前面学习的很多读写函数在调用过程中会发生阻塞。
读操作中的read、recv、recvfrom
写操作中的write、send
其他操作:accept、connect

读阻塞

以read函数为例:
进程调用read函数从套接字上读取数据,当套接字的接收缓冲区中还没有数据可读,函数read将发生阻塞。
它会一直阻塞下去,等待套接字的接收缓冲区中有数据可读。
经过一段时间后,缓冲区内接收到数据,于是内核便去唤醒该进程,通过read访问这些数据。
如果在进程阻塞过程中,对方发生故障,那这个进程将永远阻塞下去。

写阻塞

在写操作时发生阻塞的情况要比读操作少。主要发生在要写入的缓冲区的大小小于要写入的数据量的情况下。这时,写操作不进行任何拷贝工作,将发生阻塞。
一量发送缓冲区内有足够的空间,内核将唤醒进程,将数据从用户缓冲区中拷贝到相应的发送数据缓冲区。
UDP不用等待确认,没有实际的发送缓冲区,所以UDP协议中不存在发送缓冲区满的情况,在UDP套接字上执行的写操作永远都不会阻塞。

6.3 非阻塞模式I/O

当我们将一个套接字设置为非阻塞模式,我们相当于告诉了系统内核:“当我请求的I/O 操作不能够马上完成,你想让我的进程进行休眠等待的时候,不要这么做,请马上返回一个错误给我。”
当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不停地测试是否一个文件描述符有数据可读(称做polling)。应用程序不停的polling 内核来检查是否I/O操作已经就绪。这将是一个极浪费CPU 资源的操作。
这种模式使用中不普遍。
LV.6 网络编程_第22张图片

非阻塞模式的实现

fcntl()函数
当你一开始建立一个套接字描述符的时候,系统内核将其设置为阻塞IO模式。可以使用函数fcntl()设置一个套接字的标志为O_NONBLOCK 来实现非阻塞。

代码实现;

1.fcntl( )函数

int fcntl(int fd, int cmd, long arg);

  int flag;
  flag = fcntl(sockfd, F_GETFL, 0);
  flag |= O_NONBLOCK;
  fcntl(sockfd, F_SETFL, flag);
2.ioctl() 函数
   int b_on =1;
   ioctl(sock_fd, FIONBIO, &b_on);

6.4 多路复用I/O

多路复用:
基本常识:linux中每个进程默认情况下,最多可以打开1024个文件,最多有1024个文件描述符

文件描述符的特点:
	1.非负整数
	2.从最小可用的数字来分配
	3.每个进程启动时默认打开0, 1,2三个文件描述符

应用程序中同时处理多路输入输出流,若采用阻塞模式,将得不到预期的目的;
若采用非阻塞模式,对多个输入进行轮询,但又太浪费CPU时间;

若设置多个进程,分别处理一条数据通路,将新产生进程间的同步与通信问题,使程序变得更加复杂;

比较好的方法是使用I/O多路复用。其基本思想是:
先构造一张有关描述符的表,然后调用一个函数。当这些文件描述符中的一个或多个已准备好进行I/O时函数才返回。函数返回时告诉进程那个描述符已就绪,可以进行I/O操作。

实现多路复用

select()
poll()
epoll()

SELECT基本原理:select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。

POLL基本原理:poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。

EPOLL基本原理:epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。

select()
#include 
#include 
#include 

int select(int n, fd_set *read_fds, fd_set *write_fds, fd_set *except_fds, struct timeval *timeout);

maxfd 所有监控的文件描述符中最大的那一个加1
read_fds 所有要读的文件文件描述符的集合
write_fds 所有要的写文件文件描述符的集合
except_fds 其他要向我们通知的文件描述符
timeout 超时设置

Null:一直阻塞,直到有文件描述符就绪或出错
时间值为0:仅仅检测文件描述符集的状态,然后立即返回
时间值不为0:在指定时间内,如果没有事件发生,则超时返回。

在我们调用select时进程会一直阻塞直到以下的一种情况发生.
有文件可以读.
有文件可以写.
超时所设置的时间到.

//为了设置文件描述符我们要使用几个宏,宏的形式:
void FD_ZERO(fd_set *fdset)  //从fdset中清除所有的文件描述符 
void FD_SET(int fd,fd_set *fdset)  //将fd加入到fdset 
void FD_CLR(int fd,fd_set *fdset)  //将fd从fdset里面清除 
int FD_ISSET(int fd,fd_set *fdset)  // 判断fd是否在fdset集合中,由于select函数成功返回时会将未准备好的描述符位清零。通常我们使用FD_ISSET是为了检查在select函数返回后,某个描述符是否准备好,以便进行接下来的处理操作  
TCP多路复用

LV.6 网络编程_第23张图片
关键点:

  1. select( )函数里面的各个文件描述符fd_set集合的参数在select( )前后发生了变化:
    前:表示关心的文件描述符集合
    后:有数据的集合(如不是在超时还回情况下)

  2. 那么究竟是谁动了fd_set集合的奶酪?
    答曰:kernel

思考:
这种模式下,多路网络连接时候能否真正多路并发处理?如果能,请说明理由,如不能,请给出改进意见

代码(select)
net.h
#ifndef __MAKEU_NET_H__
#define __MAKEU_NET_H__

#include 
#include 
#include 
#include 
#include 
#include 
#include 			/* See NOTES */
#include 
#include 
#include 			/* superset of previous */

#include 
#include 
//#include 
#include 



#define SERV_PORT 5002
#define SERV_IP_ADDR "192.168.7.246"
#define BACKLOG 5

#define QUIT_STR "quit"
#define SERV_RESP_STR "SERVER:"

typedef int data_t;
typedef struct node{
	data_t data;
	struct node *next;
}listnode,*linklist;

linklist list_create();
int list_tail_insert(linklist H, data_t value);//head
linklist list_get(linklist H, int pos);
int list_insert(linklist H, data_t value, int pos);
int list_delete(linklist H, linklist P);
int list_show(linklist H);
linklist list_free(linklist H);


#endif


server.c(frame)
int main (void)
{
	fd_set rset;
	int maxfd = -1;

	struct timeval tout;

	fd = socket ( ...);
	bind (fd, ...);
	listen (fd, ...);

	while (1) {
		maxfd = fd;
		FD_ZERO (&rset);

		FD_SET (fd, &rset);
		//依次把已经建立好连接fd加入到集合中,记录下来最大的文件描述符maxfd
		//...FIXME!!
#if 0
		select (maxfd + 1, &rset, NULL, NULL, NULL);
#else
		tout.tv_sec = 5;
		tout.tv_usec = 0;
		select (maxfd + 1, &rset, NULL, NULL, &tout);
#endif
		if (FD_ISSET (fd, &rset)) {
			newfd = accept (fd, ....);
		}
		//依次判断已建立连接的客户端是否有数据
		//...FIXME!

	}
	return 0;
}

server.c(同学)
//select_model tcp
#include "net.h"

void *cli_data_handle(void* arg);

int main(void) 
{
	fd_set rset, rtmpset;//读集合,临时读集合
	ssize_t recv_bytes , send_bytes;
	int pzy = 1;
	int i = 0;
	int maxfd = -1;
	int fd = -1;
	int newfd = -1;
	int ret = -1;
	char buf[BUFSIZ];
	char resp_buf[BUFSIZ+10];
	struct timeval time1,time2;//原始时间,临时时间
	//TCP协议,创建套接字,第二个参数为流式套接字类型    
	if((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
		perror("socket failed");//socket出错了
		exit(1);//退出程序
	}

	//允许绑定地址快速重用	
	setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &pzy, sizeof(int));
	//服务器地址结构体填充,地址族,IP PORT
	struct sockaddr_in ser,cli;//服务器结构体,客户端结构体
	bzero(&ser, sizeof(ser));//对结构体ser清零
	ser.sin_family = AF_INET;
	ser.sin_port = htons(SERV_PORT);//本地字节序端口号变网络字节序端口号
#if 1
	ser.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY是宏,明确值是1
#else
	//本地字节序转网络字节序
	//if((inet_pton(AF_INET,SERV_IP_ADDR,(void*)&sin.sin_addr.s_addr)) !=1) {
	if((inet_pton(AF_INET,SERV_IP_ADDR,(void*)&ser.sin_addr)) != 1) {
		perror("inet_pton failed");
		exit(1);
	}
#endif
	//2.bind绑定套接字,服务器IP和端口号
	if(bind(fd, (struct sockaddr*)&ser, sizeof(ser)) < 0) {
		perror("bind failed");
		exit(1);
	}
	//	将套接字设定为被动监听状态,监听客户端的连接请求,BACKLOG为未决队列长度
	if(listen(fd, BACKLOG) < 0) {
		perror("listen failed");
		exit(1);
	}

	printf("Server is starting .....OK\n");	

	//	maxfd = fd;//将监听套接字赋值给最大套接字
	FD_ZERO(&rset);//对读集合清零
	FD_ZERO(&rtmpset);//对临时读集合清零
	FD_SET(fd, &rset);//把监听套接字fd加到读集合中
	maxfd = fd;//
	//依次把已经建立好连接fd加入到集合中,记录下来最大的文件描述符maxfd
#if 0
	select(maxfd+1, &rset, NULL, NULL, NULL);
#else
	//socklen_t len = sizeof(ser);//服务器的地址长度
	socklen_t addrlen = sizeof(cli);//客户端的地址长度
	time1.tv_sec = 5;//定时5秒
	time1.tv_usec = 0;
	while(1) {
		rtmpset = rset;//将原始读集合赋值给备份集合
		time2 = time1;//将原始时间赋值给临时时间
		if(select(maxfd+1, &rtmpset, NULL, NULL, &time2) < 0) {
			perror("select failed");
			exit(1);//退出程序
		}	
		for(i = 0; i <= maxfd; i++) {
			if(FD_ISSET(i, &rtmpset)) {//判断文件描述符是否就绪
				if(i == fd) {//监听套接字就绪
					if((newfd = accept(fd, (struct sockaddr*)&cli, &addrlen )) < 0) {
						perror("accept failed");
						return -1;
					}
					FD_SET(newfd, &rset);//将新产生的连接套接字加入到原始表单
					maxfd = (newfd > maxfd)? newfd:maxfd;//更新最大的文件描述符
					char ipv4_addr[16];//字符串最后一位以“\0”结束
					if(!inet_ntop(AF_INET, (void*)&cli.sin_addr, ipv4_addr, sizeof(cli))) {
						perror("inet_ntop failed");
						exit(1);
					}
					printf("Client (%s:%d) has connected\n",ipv4_addr, ntohs(cli.sin_port));
				}else {//连接套接字
					while(1) {
						bzero(buf, sizeof(buf));//对缓冲区清零
						do {
							recv_bytes = recv(i, buf, sizeof(buf), 0);//接收客户端信息存到buf中
						}while(recv_bytes < 0 && EINTR == errno);	
						if(recv_bytes == -1) {
							perror("recv");
							exit(1);
						}
						if(recv_bytes == 0) {
							printf("client shutdown\n");
							close(i);//关闭相应的文件描述符
							FD_CLR(i, &rset);//清除
							return -1;
						}
						printf("Client %d  %s\n", i, buf);
						bzero(resp_buf, BUFSIZ+10);
						strncpy(resp_buf, SERV_RESP_STR, strlen(SERV_RESP_STR));
						strcat(resp_buf, buf);      
						if(!strncasecmp(buf,QUIT_STR,strlen(QUIT_STR))) {//两个字符串相等,也就是用户输入了“quit”这个字符串
							printf("Client is exiting!\n");//打印客户端正在退出
							close(i);//关闭相应的文件描述符
							FD_CLR(i, &rset);//清除
							exit(1);
						} 						
						char buff[128];
						if(fgets(buff, BUFSIZ+10,stdin) == NULL) {
							perror("fgets");
						}
						send_bytes = send(i, buff,strlen(buff), 0);//发给客户端
						printf("***%d**data:%s\n",send_bytes,buff);//查看发送了几个字符给客户端
					}
				}
			}
		}
	}


#endif
	close(fd);
	return 0;
}

server.c(自己用队列实现)
#include "net.h"

int main (void)
{
	fd_set rset;
	int maxfd = -1;
	struct timeval tout;

	int fd = -1;
	struct sockaddr_in sin;

	linklist L,P,T;
	//创建一个链表用于存取文件描述符
	L = list_create();

	/* 1. 创建socket fd */
	if ((fd = socket (AF_INET, SOCK_STREAM, 0)) < 0) {
		perror ("socket");
		exit (1);
	}

	/*2. 绑定 */
	/*2.1 填充struct sockaddr_in结构体变量 */
	bzero (&sin, sizeof (sin));
	sin.sin_family = AF_INET;
	sin.sin_port = htons (SERV_PORT);	//网络字节序的端口号

	/*优化1: 让服务器程序能绑定在任意的IP上 */

	sin.sin_addr.s_addr = htonl (INADDR_ANY);

	/*2.2 绑定 */
	if (bind (fd, (struct sockaddr *) &sin, sizeof (sin)) < 0) {
		perror ("bind");
		exit (1);
	}

	/*3. 调用listen()把主动套接字变成被动套接字 */
	if (listen (fd, BACKLOG) < 0) {
		perror ("listen");
		exit (1);
	}
	printf ("Server starting....OK!\n");
	int newfd = -1;
	struct sockaddr_in cin;
	socklen_t addrlen = sizeof (cin);
	// 阻塞等待客户端连接请求 
	while(1){
		FD_ZERO(&rset);
		FD_SET(fd,&rset);
		P=L->next;
		//找出最大的句柄
		while(P){
			if(P->data>maxfd)
				//依次把已经建立好连接fd加入到集合中,记录下来最大的文件描述符maxfd			
				maxfd =P ->data;
				FD_SET(P->data,&rset);
				P=P->next;
		}
		//展示句柄列表
		puts("展示句柄列表");

		list_show(L);
		//设置超时时间
		tout.tv_sec = 5;
		tout.tv_usec = 0;
		select (maxfd + 1, &rset, NULL, NULL, &tout);
		P=L->next;
		依次判断已建立连接的客户端是否有数据
		//遍历已经存在的连接
		while(P){
			if(FD_ISSET(P->data, &rset)){
				newfd = P->data;
				//读数据
				int ret = -1;
				char buf[BUFSIZ];
				char resp_buf[BUFSIZ+10];
				bzero (buf, BUFSIZ);
				do {
					ret = read (newfd, buf, BUFSIZ - 1);
				} while (ret < 0 && EINTR == errno);
				if (ret < 0) {

					perror ("read");
					exit (1);
				}
				if (!ret) {//对方已经关闭,关闭句柄连接,删除结点信息。
					close(P->data);
					list_delete(L,P);
					break;
				}
				//输出客户端的消息请求
				printf ("Receive data: %s\n", buf);
				if (!strncasecmp (buf, QUIT_STR, strlen (QUIT_STR))) {	//用户输入了quit字符
					printf ("Client is exiting!\n");
					break;
				}
				//响应客户端的quit
				if (!strncasecmp (buf, QUIT_STR, strlen (QUIT_STR))) {	//用户输入了quit字符
					printf ("Client(fd=%d) is exiting!\n", newfd);
					break;
				}
				bzero(resp_buf, BUFSIZ+10);
				strncpy(resp_buf, SERV_RESP_STR, strlen(SERV_RESP_STR));
				strcat(resp_buf, buf); 	
				do {
					//响应客户端一个返回信息SERV_RESP_STR+客户端请求数据
					ret = write(newfd, resp_buf, strlen(resp_buf));	
				}while(ret < 0 && EINTR == errno);
			}
			P=P->next;
		}

		//检查是否有新的连接进入,判断如果是listenfd对应的文件描述符发生了事件,新的客户端发起连接请求
		if (FD_ISSET(fd, &rset)) {	
			//创建一个与s同类的新的套接口并返回句柄。
			if ((newfd = accept (fd, (struct sockaddr *) &cin, &addrlen)) < 0) {
				perror ("accept");
				break;
			}
			//将新创建的套接字的句柄加入链表
			list_tail_insert(L,newfd);
		}
	}
	close (fd);
	return 0;
}

linklist list_create() {
	linklist H;

	H = (linklist)malloc(sizeof(listnode));
	if (H == NULL) {
		printf("malloc failed\n");
		return H;
	}

	H->data = 0;
	H->next = NULL;

	return H;
}

int list_tail_insert(linklist H, data_t value) {
	linklist p;
	linklist q;

	if (H == NULL) {
		printf("H is NULL\n");
		return -1;
	}

	//1 new node p
	if ((p = (linklist)malloc(sizeof(listnode))) == NULL) {
		printf("malloc failed\n");
		return -1;
	}
	p->data = value;
	p->next = NULL;

	q = H;
	while (q->next != NULL) {
		q = q->next;
	}

	//3 insert
	q->next = p;

	return 0;
}




int list_delete(linklist H, linklist p) {
	linklist pri;

	//1
	if (H == NULL) {
		printf("H is NULL\n");
		return -1;
	}

	//2 loocate prior
	pri=H;
	while(pri){
		if(pri->next == p){
			pri->next = p->next;
			free(p);
			break;
		}
		pri=pri->next;
	}
	
	return 0;
}

int list_show(linklist H) {
	linklist p;

	if (H == NULL) {
		printf("H is NULL\n");
		return -1;
	}

	p = H;

	while (p->next != NULL) {
		printf("%d ", p->next->data);
		p = p->next;
	}
	puts("");

	return 0;
}
client.c

/*./client serv_ip serv_port */
#include "net.h"

void usage (char *s)
{
	printf ("\n%s serv_ip serv_port", s);
	printf ("\n\t serv_ip: server ip address");
	printf ("\n\t serv_port: server port(>5000)\n\n");
}

int main (int argc, char **argv)
{
	int fd = -1;

	int port = -1;
	struct sockaddr_in sin;

	if (argc != 3) {
		usage (argv[0]);
		exit (1);
	}
	/* 1. 创建socket fd */
	if ((fd = socket (AF_INET, SOCK_STREAM, 0)) < 0) {
		perror ("socket");
		exit (1);
	}

	port = atoi (argv[2]);
	if (port < 5000) {
		usage (argv[0]);
		exit (1);
	}
	/*2.连接服务器 */

	/*2.1 填充struct sockaddr_in结构体变量 */
	bzero (&sin, sizeof (sin));

	sin.sin_family = AF_INET;
	sin.sin_port = htons (port);	//网络字节序的端口号
#if 0
	sin.sin_addr.s_addr = inet_addr (SERV_IP_ADDR);
#else
	if (inet_pton (AF_INET, argv[1], (void *) &sin.sin_addr) != 1) {
		perror ("inet_pton");
		exit (1);
	}
#endif

	if (connect (fd, (struct sockaddr *) &sin, sizeof (sin)) < 0) {
		perror ("connect");
		exit (1);
	}

	printf ("Client staring...OK!\n");

	int ret = -1;
	fd_set rset;
	int maxfd = -1;
	struct timeval tout;
	char buf[BUFSIZ];

	while (1) {
		FD_ZERO (&rset);
		FD_SET (0, &rset);
		FD_SET (fd, &rset);
		maxfd = fd;

		tout.tv_sec = 5;
		tout.tv_usec = 0;

		select (maxfd + 1, &rset, NULL, NULL, &tout);
		if (FD_ISSET (0, &rset)) {	//标准键盘上有输入
			//读取键盘输入,发送到网络套接字fd
			bzero (buf, BUFSIZ);
			do {
				ret = read (0, buf, BUFSIZ - 1);
			} while (ret < 0 && EINTR == errno);
			if (ret < 0) {
				perror ("read");
				continue;
			}
			if (!ret)
				continue;

			if (write (fd, buf, strlen (buf)) < 0) {
				perror ("write() to socket");
				continue;
			}

			if (!strncasecmp (buf, QUIT_STR, strlen (QUIT_STR))) {	//用户输入了quit字符
				printf ("Client is exiting!\n");
				break;
			}
		}

		if (FD_ISSET (fd, &rset)) {	//服务器给发送过来了数据
			//读取套接字数据,处理
			bzero (buf, BUFSIZ);
			do {
				ret = read (fd, buf, BUFSIZ - 1);
			} while (ret < 0 && EINTR == errno);
			if (ret < 0) {
				perror ("read from socket");
				continue;
			}
			if (!ret)
				break;			/* 服务器关闭 */

			//There is a BUG,FIXME!!
			printf ("server said: %s\n", buf);
			if ((strlen(buf) > strlen(SERV_RESP_STR)) 
				&& !strncasecmp (buf+strlen(SERV_RESP_STR), QUIT_STR, strlen (QUIT_STR))) {	//用户输入了quit字符
				printf ("Sender Client is exiting!\n");
				break;
			}

		}
	}

	/*4.关闭套接字 */
	close (fd);
}


Makefile
# Makefile
#

#CROSS_COMPILE = arm-linux-gnu-

CC = $(CROSS_COMPILE)gcc

ifdef CROSS_COMPILE
TARGET = /opt/filesystem
endif


#DEBUG = -g -O0 -Wall
DEBUG = -g -O2
CFLAGS += $(DEBUG)

PROGS = ${patsubst %.c, %, ${wildcard *.c}} 

all : $(PROGS)

install: $(PROGS)
ifdef CROSS_COMPILE
	mkdir $(TARGET)/root/net -p
	cp $(PROGS) $(TARGET)/root/net -f
endif
%.o : %.c
	$(CC)  $(CFLAGS) -c $< -o $@

.PHONY: uninstall clean dist

uninstall :
ifdef CROSS_COMPILE
	cd $(TARGET)/root/net && rm -f $(PROGS)
endif

clean : uninstall
	- rm -f $(PROGS) core *.gz

dist: clean
	tar czf ../makeru_2_2_video_select_model.tar.gz ../2_2_video_select_model	


readme.txt

1.执行make
2.执行./server
3.同目录新开终端,执行./client 127.0.0.1 5002 ,输入test1.
4.同目录新开终端,执行./client 127.0.0.1 5002 ,输入test1.
5.关闭一个client终端

poll()
#include 
int poll(struct pollfd *ufds, unsigned int nfds, int timeout);
epoll()
service.c
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define BUFLEN 128
int createSocket(short port){
	int serverFd,clientFd;
	int len,ret,rlen;
	
	struct sockaddr_in serverAddr;
	len = sizeof(serverAddr);
	serverFd = socket(AF_INET,SOCK_STREAM,0);
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_port = htons(port);
	serverAddr.sin_addr.s_addr = 0; //inet_addr("192.168.3.120");
	int reuse = 1;
	setsockopt(serverFd,SOL_SOCKET,SO_REUSEADDR,&reuse,sizeof(int));

	ret = bind(serverFd,(struct sockaddr *)&serverAddr,len);
	if(ret<0){
		perror("Failed to bind");
		return -1;
	}
	ret = listen(serverFd,10);
	if(ret<0){
		perror("Failed to bind");
		return -1;
	}
	return serverFd;
	
	
}

int main(int argc,char ** argv){

	int sockfd;
	short port;
	int addrlen;
	struct sockaddr_in serverAddr,clientAddr;	
	addrlen = sizeof(serverAddr);
	char buf[BUFLEN];
	if(argc!=2){
		printf("Usage:%s port\n",argv[0]);
		return 0;
	}
	port = atoi(argv[1]);
	sockfd = createSocket(port);
    if(sockfd<0){
		return -1;
	}
	int epfd;
	int ret;
	int rdlen;
	int i;
	int clientFd;
	struct epoll_event event;	
	struct epoll_event events[20];
	memset(events,0,20*sizeof(struct epoll_event));
	event.events = EPOLLIN ;
	event.data.fd = sockfd;
	epfd = epoll_create(1);
        
        epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event);

        while(1){

			ret = epoll_wait(epfd, events,20, -1);   //阻塞等待时间到来
            printf("epoll_wait return=%d\n",ret);
                
            for(i=0;i<ret;i++){   //轮训到达的事件
			  if(events[i].data.fd == sockfd){//如果是监听的文件描述符有事件到来,接收新连接
				clientFd = accept(events[i].data.fd,(struct sockaddr *)&clientAddr, &addrlen);
				printf("new client %s,port=%d \n",inet_ntoa(clientAddr.sin_addr),ntohs(clientAddr.sin_port));
				event.events = EPOLLIN;
	            event.data.fd = clientFd;
				epoll_ctl(epfd, EPOLL_CTL_ADD, clientFd, &event);
			}else{   //否则,是连接的客户端事件,读取数据
				rdlen = read(events[i].data.fd,buf,BUFLEN);
				if(rdlen>0){
					printf("read buf=%s\n",buf);
				}else if (rdlen==0){//客户连接中断,删除epoll监听的客户端文件描述符
					event.events = EPOLLIN;
					event.data.fd = events[i].data.fd;
					epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, &event);					
				}
			}
		  }
        
        }

}	

作业

说下select和epoll的区别:
1、select在每次被调用的时候,都会将所有fd从用户态转换成内核态,而epoll只是在事件注册的时候拷贝一次fd而已。提高了效率。
2、对于select来说,在每次醒着的时候,都需要将整个fd遍历一遍,而对于epoll来说,只需要在current的时候挂一遍fd,然后设置一个回调函数,当设备准备完成时,就调用一个回调函数将对应的文件描述符返还给进程,所以在时间上要大大的提高于select。
3、select的文件描述符的上限默认是1024,但是epoll没有这个限制,可以远大于1024,因为它只和系统的内存大小有关,而不受限于一个定值

参考直播: http://www.makeru.com.cn/live/5413_1937.html(参考代码见课程资料)

1、写基于tcp模型的IO多路复用(select)程序,在服务器端采用select来实现客户端的多路并发

2、epoll的原理和优缺点:
原理:
在 select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一 个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait() 时便得到通知。(此处去掉了遍历文件描述符,而是通过监听回调的的机制。这正是epoll的魅力所在。)
优点:

  1. 监视的描述符数量不受限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左 右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。select的最大缺点就是进程打开的fd是有数量限制的。这对于连接数量比较大的服务器来说根本不能满足。虽然也可以选择多进程的解决方案( Apache就是这样实现的),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完美的方案。
    IO的效率不会随着监视fd的数量的增长而下降。epoll不同于select和poll轮询的方式,而是通过每个fd定义的回调函数来实现的。只有就绪的fd才会执行回调函数。
    缺点:
    如果没有大量的idle -connection或者dead-connection,epoll的效率并不会比select/poll高很多,但是当遇到大量的idle- connection,就会发现epoll的效率大大高于select/poll。

D7 TCP、IP协议原理

7.1 网络分析测试工具、封包、IP和TCP头

wireshark

WireShark是非常流行的网络封包分析工具,可以截取各种网络数据包,并显示数据包详细信息。常用于开发测试过程中各种问题定位。

Linux 安装:sudo apt-get install wireshark
启动:sudo wireshark
LV.6 网络编程_第24张图片
LV.6 网络编程_第25张图片
LV.6 网络编程_第26张图片

常用调试测试工具

使用telnet测试TCP服务器端
使用lsof
使用tcpdump
使用netstat
使用sniffer
使用wireshark
Chariot

SmartBit—硬件

TCP/IP协议网络封包格式

LV.6 网络编程_第27张图片

以太网头

物理层
LV.6 网络编程_第28张图片
LV.6 网络编程_第29张图片

IP头

网络层
LV.6 网络编程_第30张图片
IP 头固定部分20 个字节
identification 以太网进行传输包最多是1500bit ,大了需要拆散
TTL 生存周期,每过一个路由器减一
Header Checksum CRC16 认证

LV.6 网络编程_第31张图片

1、版本号字段占 4 位,给出 IP 版本号。
2、首部长度字段占 4 位,给出 IP 数据报的首部长度。
3、区分服务字段占 8 位,在旧标准种称为服务类型(Type Of Service,TOS)字段,用来指示期
望获得哪种类型的服务。
4、数据报长度字段占 16 位,指出 IP 数据报的总字节数。
5、标识字段占 16 位,标识一个 IP 数据报。用于在 IP 数据报分片和重组过程中,标识属于同一原
IP 数据报。
6、标志位字段占 3 位,其结构如下:
	最高保留位 
	DF 禁止分片标志:
		DF=0,允许路由器将该 IP 数据分片。
		DF=1,禁止路由器将该 IP 数据分片。
	MF 更多分片标志:
		MF=0,该数据报未被分片或是分片的最后一片。
		MF=1,该数据报一定是一个分片,且不是最后一个。
7、片偏移字段占 13 位,表示一个 IP 数据报分片与原 IP 数据报数据的相对偏移量,即封装的数据
分片从原整个数据报的哪个字节开始的。以 8 字节为单位。
当该字段值为 0 时,且 MF=1,则表示这是一个 IP 分片,且是第一个分片。
8、生存时间(Time-To-Live,TTL)字段占 8 位,表示 IP 数据报在网络中可以通过的路由器数(或
跳步数)。
9、上层协议字段占 8 位,指示该 IP 数据报封装的是哪个上层协议。TCP:6; UDP:17。用于
实现 IP 的多路复用与多路分解。
TCP头

传输层
面向连接,可靠传输
发送的每个字节都有编号,
LV.6 网络编程_第32张图片
LV.6 网络编程_第33张图片

1、源端口号、目的端口号字段分别占 16 位,标识发送该报文段的源端口和目的端口,用于多路复
用/分解来自或送到上层应用的数据。
2、序号字段、确认序号字段分别占 32 位。
	序号字段:该段所封装的应用层数据的第一个字节的序号。
	确认序号字段:是期望从对方接收数据的字节序号,即该序号对应的字节尚未收到。
3、首部长度字段占 4 位。指出 TCP 段的首部长度,以 4 字节为计算单位。该字段最大取值为 15,
即 TCP 最大首部长度为 60 字节。
4、保留字段占 6 位。保留为今后使用,目前值为 0。
5、URG、ACK、PSH、RST、SYN、FIN 各占 1 位。
	紧急 URG=1,紧急指针字段有效,优先传送。
	确认 ACK=1,确认序号字段有效;ACK=0 时,确认序号字段无效。
	推送 PSH=1,尽快将报文段中的数据交付接收应用进程,不要等缓存满了再交付。
	复位 RST=1,TCP 连接出现严重差错,释放连接,再重新建立 TCP 连接。
	同步 SYN=1,该 TCP 报文段是一个建立新连接请求控制段或者同意建立新连接的确认段。
	终止 FIN=1,TCP 报文段的发送端数据已经发送完毕,请求释放连接。
6、接收窗口字段占 16 位。向对方通告我方接收窗口的大小(单位为字节),用于实现 TCP 流量控
制。
7、校验和字段占 16 位。校验和字段范围和计算方法与 UDP 相同。TCP 协议号是 6。
8、紧急指针字段占 16 位。该字段只有 URG=1 时才有效。指出在本 TCP 报文段中紧急数据共有多
少个字节。
9、选项字段长度可变。最长为 40 字节。
10、填充字段,取值全为 0,目的是为了整个首部长度是 4 字节的整倍数。

实现可靠数据传输有 5 种措施,依据这些措施设计的可靠数据传输协议中最具代表性的是停-等协议、滑动窗口协议

实现可靠数据传输的措施主要包括以下几种:
	1) 差错检测:利用差错编码实现数据包传输过程中的比特差错检测(甚至纠正)。
	2) 确认:接收方向发送方反馈接收状态。
	3) 重传:发送方重新发送接收方没有正确接收的数据。
	4) 序号:确保数据按序提交。
	5) 计时器:解决数据丢失问题。
		
停-等协议的工作过程。
答案及解析:停-等协议的基本工作过程是:发送方发送经过差错编码和编号的报文段,等待接收方
的确认;接收方如果正确接收报文段,即差错检测无误且序号正确,则接收报文段,并向发送方发
送 ACK,否则丢弃报文段,并向发送方发送 NAK;发送方如果收到 ACK,则继续发送后续报文段,否
则重发刚刚发送的报文段。

滑动窗口协议的基本工作过程
1、分组连续编号;
2、以流水线方式依次发送分组;
3、接收方接收分组,按分组序
号向上有序提交;
4、通过确认向发送方通告正确
接收的分组序号;
5、发送方根据收到的 ACK 的序
号以及计时器的,重新发送或者
继续发送新分组。

滑动窗口协议根据采用的确认、计时、窗口大小等机制的不同,可以设计不同的滑动窗口。
	回退 N 步(GBN)协议
	选择重传(SR)协议
UDP头

LV.6 网络编程_第34张图片

LV.6 网络编程_第35张图片

7.2 TCP握手过程

TCP三次/四次握手

LV.6 网络编程_第36张图片

作业

  1. 请解释TCP的可靠传输原理

    为了实现可靠性传输,需要考虑很多事情,例如数据的破坏、丢包、重复以及分片顺序混乱等问题。如不能解决这些问题,也就无从谈起可靠传输。那么,TCP 是通过序列号、确认应答、重发控制、连接管理以及窗口控制等机制实现可靠性传输的。
    TCP 实现可靠传输的方式之一,是通过序列号与确认应答。在 TCP 中,当发送端的数据到达接收主机时,接收端主机会返回一个确认应答消息,表示已收到消息。但在错综复杂的网络,并不一定能如上图那么顺利能正常的数据传输,万一数据在传输过程中丢失了呢?所以 TCP 针对数据包丢失的情况,会用重传机制解决。

  2. 请叙述在client/server模型中,TCP的三次握手和四次握手过程

(1)第一次握手:Client将标志位SYN (synchronous建立联机)置为1,随机产生一个值seq(Sequence number (顺序号码))=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。
(2)第二次握手:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK(acknowledgement 确认)都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。
(3)第三次握手:Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。

(1)第一次挥手:Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。
(2)第二次挥手:Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。
(3)第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。
(4)第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。

D8 网络编程扩展上

8.1域名解析

LV.6 网络编程_第37张图片

网络信息检索函数

gethostname() 获得主机名
getpeername() 获得与套接口相连的远程协议地址
getsockname() 获得本地套接口协议地址
gethostbyname() 根据主机名取得主机信息
endhostent()

gethostbyaddr() 根据主机地址取得主机信息
getprotobyname() 根据协议名取得主机协议信息
getprotobynumber() 根据协议号取得主机协议信息
getservbyname() 根据服务名取得相关服务信息
getservbyport() 根据端口号取得相关服务信息

gethostbyname()

使用步骤:
  1. netdb.h
  2. struct hostent *hs = NULL;
  3. hs = gethostbyname (argv[1]))
  4. sin.sin_addr.s_addr = *(uint32_t *) hs->h_addr;
    endhostent ();
说明

IPv4中使用gethostbyname()函数完成主机名到地址解析,这个函数仅仅支持IPv4,且不允许调用者指定所需地址类型的任何信息,返回的结构只包含了用于存储IPv4地址的空间。IPv6中引入了getaddrinfo()的新API,它是协议无关的,既可用于IPv4也可用于IPv6。

代码
关键代码
#include "net.h"
...h

struct sockaddr_in sin;
struct hostent *hs = NULL;

if ((hs = gethostbyname (argv[1])) == NULL) {
	herror ("gethostbyname error");
	exit (1);
}
...
sin.sin_addr.s_addr = *(uint32_t *) hs->h_addr;
endhostent ();//此函数告诉系统您不再期望使用gethostent从hosts文件中读取条目。
hs = NULL;
...
client.c

/*./client serv_name serv_port */
#include 
#include "net.h"

void usage (char *s)
{
	printf ("\n%s serv_ip serv_port", s);
	printf ("\n\t serv_name: server domain name or ip address");
	printf ("\n\t serv_port: server port(>5000)\n\n");
}

int main (int argc, char **argv)
{
	int fd = -1;

	int port = -1;
	struct sockaddr_in sin;
	struct hostent *hs = NULL;

	if (argc != 3) {
		usage (argv[0]);
		exit (1);
	}

	port = atoi (argv[2]);
	if (port < 5000) {
		usage (argv[0]);
		exit (1);
	}

	if ((hs = gethostbyname (argv[1])) == NULL) {
		herror ("gethostbyname error");
		exit (1);
	}

	/* 1. 创建socket fd */
	if ((fd = socket (AF_INET, SOCK_STREAM, 0)) < 0) {
		perror ("socket");
		exit (1);
	}

	/*2.连接服务器 */

	/*2.1 填充struct sockaddr_in结构体变量 */
	bzero (&sin, sizeof (sin));

	sin.sin_family = AF_INET;
	sin.sin_port = htons (port);	//网络字节序的端口号
#if 1
	sin.sin_addr.s_addr = *(uint32_t *) hs->h_addr;
	endhostent ();
	hs = NULL;
#else
	if (inet_pton (AF_INET, argv[1], (void *) &sin.sin_addr) != 1) {
		perror ("inet_pton");
		exit (1);
	}
#endif


	if (connect (fd, (struct sockaddr *) &sin, sizeof (sin)) < 0) {
		perror ("connect");
		exit (1);
	}

	printf ("Client staring...OK!\n");

	int ret = -1;
	fd_set rset;
	int maxfd = -1;
	struct timeval tout;
	char buf[BUFSIZ];

	while (1) {
		FD_ZERO (&rset);
		FD_SET (0, &rset);
		FD_SET (fd, &rset);
		maxfd = fd;

		tout.tv_sec = 5;
		tout.tv_usec = 0;

		select (maxfd + 1, &rset, NULL, NULL, &tout);
		if (FD_ISSET (0, &rset)) {	//标准键盘上有输入
			//读取键盘输入,发送到网络套接字fd
			bzero (buf, BUFSIZ);
			do {
				ret = read (0, buf, BUFSIZ - 1);
			} while (ret < 0 && EINTR == errno);
			if (ret < 0) {
				perror ("read");
				continue;
			}
			if (!ret)
				continue;

			if (write (fd, buf, strlen (buf)) < 0) {
				perror ("write() to socket");
				continue;
			}

			if (!strncasecmp (buf, QUIT_STR, strlen (QUIT_STR))) {	//用户输入了quit字符
				printf ("Client is exiting!\n");
				break;
			}
		}

		if (FD_ISSET (fd, &rset)) {	//服务器给发送过来了数据
			//读取套接字数据,处理
			bzero (buf, BUFSIZ);
			do {
				ret = read (fd, buf, BUFSIZ - 1);
			} while (ret < 0 && EINTR == errno);
			if (ret < 0) {
				perror ("read from socket");
				continue;
			}
			if (!ret)
				break;			/* 服务器关闭 */

			//There is a BUG,FIXME!!
			printf ("server said: %s\n", buf);
			if ((strlen (buf) > strlen (SERV_RESP_STR))
				&& !strncasecmp (buf + strlen (SERV_RESP_STR), QUIT_STR, strlen (QUIT_STR))) {	//用户输入了quit字符
				printf ("Sender Client is exiting!\n");
				break;
			}

		}
	}

	/*4.关闭套接字 */
	close (fd);
}

网络属性设置

getsockopt和setsockopt

#include           /* See NOTES */
#include 
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen); //可以用来设置快速重用

level指定控制套接字的层次.可以取三种值:
1)SOL_SOCKET:通用套接字选项.(应用层)
2)IPPROTO_TCP:TCP选项. (传输层)
3)IPPROTO_IP:IP选项. (网络层)

optname指定控制的方式(选项的名称),我们下面详细解释 
optval获得或者是设置套接字选项.根据选项名称的数据类型进行转换 
LV.6 网络编程_第38张图片
举例:
在这里插入图片描述

在这里插入图片描述

LV.6 网络编程_第39张图片
其中timeval的定义如下:

 struct timeval {
            long tv_sec;                /* seconds : 秒*/
            long tv_usec;               /* microseconds: 微妙 */
 };

8.2 网络超时

在网络通信中,很多操作会使得进程阻塞

TCP套接字中的recv/accept/connect

UDP套接字中的recvfrom

超时检测的必要性
避免进程在没有数据时无限制地阻塞
当设定的时间到时,进程从原操作返回继续运行

网络超时检测

设置socket的属性 SO_RCVTIMEO

参考代码如下

//设置socket的属性 SO_RCVTIMEO
//参考代码如下
struct timeval  tv;
tv.tv_sec = 5;   //  设置5秒时间
tv.tv_usec = 0;
setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO,  &tv,  
                  sizeof(tv));   //  设置接收超时
 recv() / recvfrom()    //   从socket读取数据
用select检测socket是否’ready’

参考代码如下

struct fd_set rdfs;
struct timeval  tv = {5 , 0};   // 设置5秒时间

FD_ZERO(&rdfs);
FD_SET(sockfd, &rdfs);

if (select(sockfd+1, &rdfs, NULL, NULL, &tv) > 0)   // socket就绪
{
      recv() /  recvfrom()    //  从socket读取数据
}
设置定时器(timer), 捕捉SIGALRM信号

参考代码如下

void  handler(int signo)     {   return;  }

struct sigaction  act;
sigaction(SIGALRM, NULL, &act);
act.sa_handler = handler;
act.sa_flags &= ~SA_RESTART;  //清除掉SIGALRM信号的SA_RESTART
sigaction(SIGALRM, &act, NULL);
alarm(5);
if (recv(,,,) < 0) ……

心跳探测

方法一: 数据交互双方隔一段时间,一方发送一点数据到对方,对方给出特定的应答。如超过设定次数大小的时间内还是没有应答,这时候认为异常

方法2:改变套接字的属性来实现

//函数定义:
void setKeepAlive (int sockfd, int attr_on, socklen_t idle_time, socklen_t interval, socklen_t cnt)
{
      setsockopt (sockfd, SOL_SOCKET, SO_KEEPALIVE, (const char *) &attr_on, sizeof (attr_on));
      setsockopt (sockfd, SOL_TCP, TCP_KEEPIDLE, (const char *) &idle_time, sizeof (idle_time));
      setsockopt (sockfd, SOL_TCP, TCP_KEEPINTVL, (const char *) &interval, sizeof (interval));
      setsockopt (sockfd, SOL_TCP, TCP_KEEPCNT, (const char *) &cnt, sizeof (cnt));
}

//函数调用
int keepAlive = 1;//设定keepalive
int keepIdle = 5;//开始首次keepAlive探测前的TCP空闭时间
int keepInterval = 5//两次keepAlive探测间的时间间隔
int keepCount = 3;//判定断开前的keepAlive探测次数
setKeepAlive (newfd, keepAlive , keepIdle, keepInterval, keepCount );

思考:

试总结如何在linux中动态检查到是否有网络以及网络中途的掉线/连接的检查?

提示:
1.应用层
心跳检测
2.内核中
网卡驱动中 2.6内核里面,使能1s的周期性检查定时器
网卡硬件或者我们通过GPIO,插拔网线时候产生中断,处理相应中断 //立即检测到

D9 网络编程扩展下

9.1 广播和组播

广播和组播一定是UDP数据包

广播

用于局域网内部一个发送,所有人接收

前面介绍的数据包发送方式只有一个接受方,称为单播
如果同时发给局域网中的所有主机,称为广播
只有用户数据报(使用UDP协议)套接字才能广播
广播地址
以192.168.1.0 (255.255.255.0) 网段为例,最大的主机地址192.168.1.255代表该网段的广播地址
发到该地址的数据包被所有的主机接收
255.255.255.255 全网广播,在所有网段中都代表广播地址
LV.6 网络编程_第40张图片

广播发送

创建用户数据报套接字
缺省创建的套接字不允许广播数据包,需要设置属性
setsockopt可以设置套接字属性
接收方地址指定为广播地址
指定端口信息
发送数据包

setsockopt

int setsockopt(int s, int level, int optname, const void *optval, socklen_t optlen);
头文件:
level : 选项级别(例如SOL_SOCKET)
optname : 选项名(例如SO_BROADCAST)
optval : 存放选项值的缓冲区的地址
optlen : 缓冲区长度
返回值:成功返回0 失败返回-1并设置errno

广播发送示例

sockfd = socket(,);
……
int on = 1;
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on));
……
sendto(;;;;;

广播接收

创建用户数据报套接字
绑定本机IP地址和端口
绑定的端口必须和发送方指定的端口相同
等待接收数据

代码
Makefile
# Makefile
#

#CROSS_COMPILE = arm-linux-gnu-

CC = $(CROSS_COMPILE)gcc

ifdef CROSS_COMPILE
TARGET = /opt/filesystem
endif


#DEBUG = -g -O0 -Wall
DEBUG = -g -O2
CFLAGS += $(DEBUG)

PROGS = ${patsubst %.c, %, ${wildcard *.c}} 

all : $(PROGS)

install: $(PROGS)
ifdef CROSS_COMPILE
	mkdir $(TARGET)/root/long_term/io -p
	cp $(PROGS) $(TARGET)/root/long_term/io -f
endif
%.o : %.c
	$(CC)  $(CFLAGS) -c $< -o $@

.PHONY: uninstall clean dist

uninstall :
ifdef CROSS_COMPILE
	cd $(TARGET)/root/long_term/io && rm -f $(PROGS)
endif

clean : uninstall
	- rm -f $(PROGS) core *.gz

dist: clean
	tar czf ../makeru_boardcast_demo.tar.gz ../boardcast_demo
	
net.h
#ifndef __MAKEU_NET_H__
#define __MAKEU_NET_H__

#include 
#include 
#include 
#include 
#include 
#include 
#include 			/* See NOTES */
#include 
#include 
#include 			/* superset of previous */

#define SERV_PORT 5003
#define SERV_IP_ADDR "192.168.7.246"

#define QUIT_STR "quit"

#endif
receiver.c
#include "net.h"

int main (void)
{

	int fd = -1;
	struct sockaddr_in sin;

	/* 1. 创建socket fd */
	if ((fd = socket (AF_INET, SOCK_DGRAM, 0)) < 0) {	//udp程序
		perror ("socket");
		exit (1);
	}

	/* 2. 允许绑定地址快速重用 */
	int b_reuse = 1;
	setsockopt (fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int));

	/*2. 绑定 */
	/*2.1 填充struct sockaddr_in结构体变量 */
	bzero (&sin, sizeof (sin));
	sin.sin_family = AF_INET;
	sin.sin_port = htons (SERV_PORT);	//网络字节序的端口号

	/* 让服务器程序能绑定在任意的IP上 */
#if 1
	sin.sin_addr.s_addr = htonl (INADDR_ANY);
#else
	if (inet_pton (AF_INET, SERV_IP_ADDR, (void *) &sin.sin_addr) != 1) {
		perror ("inet_pton");
		exit (1);
	}
#endif
	/*2.2 绑定 */
	if (bind (fd, (struct sockaddr *) &sin, sizeof (sin)) < 0) {
		perror ("bind");
		exit (1);
	}

	char buf[BUFSIZ];
	struct sockaddr_in cin;
	socklen_t addrlen = sizeof (cin);
	printf ("\nBoardcast receiver started!\n");
	while (1) {
		bzero (buf, BUFSIZ);
		if (recvfrom (fd, buf, BUFSIZ - 1, 0, (struct sockaddr *) &cin, &addrlen) < 0) {
			perror ("recvfrom");
			continue;
		}

		char ipv4_addr[16];
		if (!inet_ntop (AF_INET, (void *) &cin.sin_addr, ipv4_addr, sizeof (cin))) {
			perror ("inet_ntop");
			exit (1);
		}

		printf ("Recived boardcast data:%s\n",  buf);

		if (!strncasecmp (buf, QUIT_STR, strlen (QUIT_STR))) {	//用户输入了quit字符
			printf ("Sender(%s:%d) is exiting!\n", ipv4_addr, ntohs (cin.sin_port));
		}

	}

	close (fd);

	return 0;
}

sender.c

/*udp demo */

/* usage:
 * ./client serv_ip serv_port 
*/
#include "net.h"
void usage (char *s)
{
	printf ("\nThis is udp demo!\n");
	printf ("\nUsage:\n\t %s serv_ip serv_port", s);
	printf ("\n\t serv_ip: udp server ip address");
	printf ("\n\t serv_port: udp server port(serv_port > 5000)\n\n");
}

int main (int argc, char *argv[])
{
	int fd = -1;
	int port = SERV_PORT;

	port = atoi (argv[2]);
	if (port < 0 || (port > 0 && port <= 5000)) {
		usage (argv[0]);
		exit (1);
	}
	struct sockaddr_in sin;
	if (argc != 3) {
		usage (argv[0]);
		exit (1);
	}

	/* 1. 创建socket fd */
	if ((fd = socket (AF_INET, SOCK_DGRAM, 0)) < 0) {	//UDP编程
		perror ("socket");
		exit (1);
	}

	/* 允许广播设置 */
	int b_br = 1;
	setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &b_br, sizeof(int));

	/*2.1 填充struct sockaddr_in结构体变量 */
	bzero (&sin, sizeof (sin));

	sin.sin_family = AF_INET;
	sin.sin_port = htons (SERV_PORT);	//网络字节序的端口号
#if 0
	sin.sin_addr.s_addr = inet_addr (argv[1]);
#else
	if (inet_pton (AF_INET, argv[1], (void *) &sin.sin_addr) != 1) {
		perror ("inet_pton");
		exit (1);
	}
#endif
	printf ("broadcast demo started!\n");
	char buf[BUFSIZ];
	while (1) {
		fprintf (stderr, "pls input string:");
		bzero (buf, BUFSIZ);
		if (fgets (buf, BUFSIZ - 1, stdin) == NULL) {
			perror ("fgets");
			continue;
		}

		sendto (fd, buf, strlen (buf), 0, (struct sockaddr *) &sin, sizeof (sin));

		if (!strncasecmp (buf, QUIT_STR, strlen (QUIT_STR))) {	//用户输入了quit字符
			printf ("Client is exited!\n");
			break;
		}

	}
	close (fd);
	return 0;
}

组播

单播方式只能发给一个接收方,组播是一个人发送,加入到多播组的人接受数据。
广播方式发给所有的主机。过多的广播会大量占用网络带宽,造成广播风暴,影响正常的通信。
组播(又称为多播)是一种折中的方式。只有加入某个多播组的主机才能收到数据。
多播方式既可以发给多个主机,又能避免象广播那样带来过多的负载(每台主机要到传输层才能判断广播包是否要处理)
组播的IP地址: 224.0.0.1~239.255.255.254(中间除掉广播)
组播必须基于UDP的编程方法

LV.6 网络编程_第41张图片

网络地址

A类地址
第1字节为网络地址,其他3个字节为主机地址。第1字节的最高位固定为0
1.0.0.1 – 126.255.255.255

B类地址
第1字节和第2字节是网络地址,其他2个字节是主机地址。第1字节的前两位固定为10
128.0.0.1 – 191.255.255.255

C类地址
前3个字节是网络地址,最后1个字节是主机地址。第1字节的前3位固定为110
192.0.0.1 – 223.255.255.255

D类地址(组播地址)
不分网络地址和主机地址,第1字节的前4位固定为1110
224.0.0.1 – 239.255.255.255

组播发送

创建用户数据报套接字
接收方地址指定为组播地址
指定端口信息
发送数据包

组播接收

创建用户数据报套接字
加入多播组
绑定本机IP地址和端口
绑定的端口必须和发送方指定的端口相同
等待接收数据

加入多播组

struct ip_mreq
{
struct in_addr imr_multiaddr;
struct in_addr imr_interface;
};

struct ip_mreq mreq;
bzero(&mreq, sizeof(mreq));
mreq.imr_multiaddr.s_addr = inet_addr(“235.10.10.3”);
mreq.imr_interface.s_addr = htonl(INADDR_ANY);

setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));

代码
Makefile
# Makefile
#

#CROSS_COMPILE = arm-linux-gnu-

CC = $(CROSS_COMPILE)gcc

ifdef CROSS_COMPILE
TARGET = /opt/filesystem
endif


#DEBUG = -g -O0 -Wall
DEBUG = -g -O2
CFLAGS += $(DEBUG)

PROGS = ${patsubst %.c, %, ${wildcard *.c}} 

all : $(PROGS)

install: $(PROGS)
ifdef CROSS_COMPILE
	mkdir $(TARGET)/root/net -p
	cp $(PROGS) $(TARGET)/root/net -f
endif
%.o : %.c
	$(CC)  $(CFLAGS) -c $< -o $@

.PHONY: uninstall clean dist

uninstall :
ifdef CROSS_COMPILE
	cd $(TARGET)/root/net && rm -f $(PROGS)
endif

clean : uninstall
	- rm -f $(PROGS) core *.gz

dist: clean
	tar czf ../makeru_multicast_demo.tar.gz ../multicast_demo	

net.h
#ifndef __MAKEU_NET_H__
#define __MAKEU_NET_H__

#include 
#include 
#include 
#include 
#include 
#include 
#include 			/* See NOTES */
#include 
#include 
#include 			/* superset of previous */

#define SERV_PORT 5004
#define MULTICAST_IP "235.10.10.3"

#define QUIT_STR "quit"

#endif

receiver.c
#include "net.h"

int main (void)
{

	int fd = -1;
	struct sockaddr_in sin;

	/* 1. 创建socket fd */
	if ((fd = socket (AF_INET, SOCK_DGRAM, 0)) < 0) {	//udp程序
		perror ("socket");
		exit (1);
	}

	/* 2. 允许绑定地址快速重用 */
	int b_reuse = 1;
	setsockopt (fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int));

	/*加入多播组*/
	struct ip_mreq mreq;
	bzero(&mreq, sizeof(mreq));
	mreq.imr_multiaddr.s_addr = inet_addr(MULTICAST_IP);
	mreq.imr_interface.s_addr = htonl(INADDR_ANY);
	
	setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP,&mreq, sizeof(mreq));

	/*2. 绑定 */
	/*2.1 填充struct sockaddr_in结构体变量 */
	bzero (&sin, sizeof (sin));
	sin.sin_family = AF_INET;
	sin.sin_port = htons (SERV_PORT);	//网络字节序的端口号

	/* 让服务器程序能绑定在任意的IP上 */
#if 1
	sin.sin_addr.s_addr = htonl (INADDR_ANY);
#else
	if (inet_pton (AF_INET, SERV_IP_ADDR, (void *) &sin.sin_addr) != 1) {
		perror ("inet_pton");
		exit (1);
	}
#endif
	/*2.2 绑定 */
	if (bind (fd, (struct sockaddr *) &sin, sizeof (sin)) < 0) {
		perror ("bind");
		exit (1);
	}

	char buf[BUFSIZ];
	struct sockaddr_in cin;
	socklen_t addrlen = sizeof (cin);
	printf ("\nmulticast demo started!\n");
	while (1) {
		bzero (buf, BUFSIZ);
		if (recvfrom (fd, buf, BUFSIZ - 1, 0, (struct sockaddr *) &cin, &addrlen) < 0) {
			perror ("recvfrom");
			continue;
		}

		char ipv4_addr[16];
		if (!inet_ntop (AF_INET, (void *) &cin.sin_addr, ipv4_addr, sizeof (cin))) {
			perror ("inet_ntop");
			exit (1);
		}

		printf ("Recived from(%s:%d), data:%s", ipv4_addr, ntohs (cin.sin_port), buf);

		if (!strncasecmp (buf, QUIT_STR, strlen (QUIT_STR))) {	//用户输入了quit字符
			printf ("Client(%s:%d) is exiting!\n", ipv4_addr, ntohs (cin.sin_port));
		}

	}

	close (fd);

	return 0;
}

sender.c

/*udp demo */

/* usage:
 * ./client serv_ip serv_port 
*/
#include "net.h"
void usage (char *s)
{
	printf ("\nThis is multicast demo!\n");
	printf ("\nUsage:\n\t %s serv_ip serv_port", s);
	printf ("\n\t serv_ip: udp server ip address(between 224~239 segment)");
	printf ("\n\t serv_port: udp server port(serv_port > 5000)\n\n");
}

int main (int argc, char *argv[])
{
	int fd = -1;
	int port = SERV_PORT;

	port = atoi (argv[2]);
	if (port < 0 || (port > 0 && port <= 5000)) {
		usage (argv[0]);
		exit (1);
	}
	struct sockaddr_in sin;
	if (argc != 3) {
		usage (argv[0]);
		exit (1);
	}

	/* 1. 创建socket fd */
	if ((fd = socket (AF_INET, SOCK_DGRAM, 0)) < 0) {	//UDP编程
		perror ("socket");
		exit (1);
	}

	/*2.1 填充struct sockaddr_in结构体变量 */
	bzero (&sin, sizeof (sin));

	sin.sin_family = AF_INET;
	sin.sin_port = htons (SERV_PORT);	//网络字节序的端口号
#if 0
	sin.sin_addr.s_addr = inet_addr (argv[1]);
#else
	if (inet_pton (AF_INET, argv[1], (void *) &sin.sin_addr) != 1) {
		perror ("inet_pton");
		exit (1);
	}
#endif
	printf ("multicast started!\n");
	char buf[BUFSIZ];
	while (1) {
		fprintf (stderr, "pls input string:");
		bzero (buf, BUFSIZ);
		if (fgets (buf, BUFSIZ - 1, stdin) == NULL) {
			perror ("fgets");
			continue;
		}

		sendto (fd, buf, strlen (buf), 0, (struct sockaddr *) &sin, sizeof (sin));

		if (!strncasecmp (buf, QUIT_STR, strlen (QUIT_STR))) {	//用户输入了quit字符
			printf ("Client is exited!\n");
			break;
		}

	}
	close (fd);
	return 0;
}

9.2 UNIX域套接字

socket同样可以用于本地进程间的通信

创建套接字时使用本地协议PF_UNIX(或PF_LOCAL)。只用于同一主机内部进程间通信的socket应使用的协议族
socket(AF_LOCAL, SOCK_STREAM, 0)
socket(AF_LOCAL, SOCK_DGRAM, 0)

分为流式套接字和用户数据报套接字
和其他进程间通信方式3相比使用方便、效率更高
常用于前后台进程通信

本地地址结构
struct sockaddr_un //
{
sa_family_t sun_family;
char sun_path[108]; // 套接字文件的路径
};

填充地址结构
struct sockaddr_un myaddr;
bzero(&myaddr, sizeof(myaddr));
myaddr.sun_family = AF_UNIX;
strcpy(myaddr.sun_path, “/tmp/mysocket”);

UNIX域(流式)套接字

服务器端

LV.6 网络编程_第42张图片

客户端

LV.6 网络编程_第43张图片

UNIX域(用户数据报)套接字

服务器端

LV.6 网络编程_第44张图片

客户端

在这里插入图片描述

代码

Makefile
# Makefile
#

#CROSS_COMPILE = arm-linux-gnu-

CC = $(CROSS_COMPILE)gcc

ifdef CROSS_COMPILE
TARGET = /opt/filesystem
endif


#DEBUG = -g -O0 -Wall
DEBUG = -g -O2
CFLAGS += $(DEBUG)

PROGS = ${patsubst %.c, %, ${wildcard *.c}} 

all : $(PROGS)

install: $(PROGS)
ifdef CROSS_COMPILE
	mkdir $(TARGET)/root/net -p
	cp $(PROGS) $(TARGET)/root/net -f
endif
%.o : %.c
	$(CC)  $(CFLAGS) -c $< -o $@

.PHONY: uninstall clean dist

uninstall :
ifdef CROSS_COMPILE
	cd $(TARGET)/root/net && rm -f $(PROGS)
endif

clean : uninstall
	- rm -f $(PROGS) core *.gz

dist: clean
	tar czf ../makeru_unix_domain_demo.tar.gz ../unix_domain

net.h
#ifndef __MAKEU_NET_H__
#define __MAKEU_NET_H__

#include 
#include 
#include 
#include 
#include 
#include 
#include 			/* See NOTES */
#include 
#include 
#include 			/* superset of previous */

#include 
#include 
//#include 
#include 
#include 

#define UNIX_DOMAIN_FILE "/tmp/my_domain_file.1"

#define BACKLOG 5


#define QUIT_STR "quit"
#define SERV_RESP_STR "SERVER:"
#endif

server.c
#include 
#include 
#include "net.h"

void cli_data_handle (void *arg);

void sig_child_handle(int signo)
{
	if(SIGCHLD == signo) {
		waitpid(-1, NULL,  WNOHANG);
	}
}
int main (void)
{

	int fd = -1;
	
	signal(SIGCHLD, sig_child_handle);	

	/* 1. 创建socket fd */
	if ((fd = socket (AF_LOCAL, SOCK_STREAM, 0)) < 0) { //基于本地的TCP通信
		perror ("socket");
		exit (1);
	}

	/* 允许绑定地址快速重用 */
	int b_reuse = 1;
	setsockopt (fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int));
	
	/* 2.1 填充sockaddr_un结构体变量 */
	struct sockaddr_un sun;
	bzero(&sun, sizeof(sun));
	sun.sun_family = AF_LOCAL;
	
	/* 如果UNIX_DOMAIN_FILE所指向的文件存在,则删除 */
	if(!access(UNIX_DOMAIN_FILE, F_OK)) {
		unlink(UNIX_DOMAIN_FILE);
	}
	strncpy(sun.sun_path, UNIX_DOMAIN_FILE, strlen( UNIX_DOMAIN_FILE));
	

	/*2.2 绑定 */
	if (bind (fd, (struct sockaddr *) &sun, sizeof (sun)) < 0) {
		perror ("bind");
		exit (1);
	}

	/*3. 调用listen()把主动套接字变成被动套接字 */
	if (listen (fd, BACKLOG) < 0) {
		perror ("listen");
		exit (1);
	}
	printf ("Unix domain server starting....OK!\n");
	int newfd = -1;
	/*4. 阻塞等待客户端连接请求 */
	while(1) {
		pid_t pid = -1;
		if ((newfd = accept (fd, NULL,NULL)) < 0) {
                        perror ("accept");
                        break;
                }
		/*创建一个子进程用于处理已建立连接的客户的交互数据*/
		if((pid = fork()) < 0) {
			perror("fork");
			break;
		}
		
		if(0 == pid) {  //子进程中
			close(fd);

               	 	printf ("Clinet is connected!\n");	
			cli_data_handle(&newfd);		
			return 0;	
		
		} else { //实际上此处 pid >0, 父进程中 
			close(newfd);
		}
		

	}		


	close (fd);
	return 0;
}

void cli_data_handle (void *arg)
{
	int newfd = *(int *) arg;

	printf ("Child handling process: newfd =%d\n", newfd);

	//..和newfd进行数据读写


	int ret = -1;
	char buf[BUFSIZ];
	char resp_buf[BUFSIZ+10];
	while (1) {
		bzero (buf, BUFSIZ);
		do {
			ret = read (newfd, buf, BUFSIZ - 1);
		} while (ret < 0 && EINTR == errno);
		if (ret < 0) {

			perror ("read");
			exit (1);
		}
		if (!ret) {				//对方已经关闭
			break;
		}
		printf ("Receive data: %s\n", buf);
		
		if (!strncasecmp (buf, QUIT_STR, strlen (QUIT_STR))) {	//用户输入了quit字符
			printf ("Client(fd=%d) is exiting!\n", newfd);
			break;
		}

		bzero(resp_buf, BUFSIZ+10);
		
		strncpy(resp_buf, SERV_RESP_STR, strlen(SERV_RESP_STR));
		strcat(resp_buf, buf); 	
		do {
			ret = write(newfd, resp_buf, strlen(resp_buf));	
		}while(ret < 0 && EINTR == errno);
		
	}
	close (newfd);

}

client.c

/*./client unix_domain_file */
#include "net.h"

void usage (char *s)
{
	printf ("\n%s unix_domain_file\n\n", s);
}

int main (int argc, char **argv)
{
	int fd = -1;

	int port = -1;
	struct sockaddr_un sun;

	if (argc != 2) {
		usage (argv[0]);
		exit (1);
	}
	/* 1. 创建socket fd */
	if ((fd = socket (AF_LOCAL, SOCK_STREAM, 0)) < 0) {
		perror ("socket");
		exit (1);
	}

	/*2.连接服务器 */

	/*2.1 填充struct sockaddr_un结构体变量 */
	bzero (&sun, sizeof (sun));

	sun.sun_family = AF_LOCAL;

	
	/*确保UNIX_DOMAIN_FILE要先存在并且可写,不存在则退出 */
	if( access(UNIX_DOMAIN_FILE, F_OK| W_OK) < 0){
		exit(1);
	}
	strncpy(sun.sun_path, UNIX_DOMAIN_FILE, strlen( UNIX_DOMAIN_FILE));


	if (connect (fd, (struct sockaddr *) &sun, sizeof (sun)) < 0) {
		perror ("connect");
		exit (1);
	}

	printf ("Unix domain Client staring...OK!\n");

	int ret = -1;
	fd_set rset;
	int maxfd = -1;
	struct timeval tout;
	char buf[BUFSIZ];

	while (1) {
		FD_ZERO (&rset);
		FD_SET (0, &rset);
		FD_SET (fd, &rset);
		maxfd = fd;

		tout.tv_sec = 5;
		tout.tv_usec = 0;

		select (maxfd + 1, &rset, NULL, NULL, &tout);
		if (FD_ISSET (0, &rset)) {	//标准键盘上有输入
			//读取键盘输入,发送到网络套接字fd
			bzero (buf, BUFSIZ);
			do {
				ret = read (0, buf, BUFSIZ - 1);
			} while (ret < 0 && EINTR == errno);
			if (ret < 0) {
				perror ("read");
				continue;
			}
			if (!ret)
				continue;

			if (write (fd, buf, strlen (buf)) < 0) {
				perror ("write() to socket");
				continue;
			}

			if (!strncasecmp (buf, QUIT_STR, strlen (QUIT_STR))) {	//用户输入了quit字符
				printf ("Client is exiting!\n");
				break;
			}
		}

		if (FD_ISSET (fd, &rset)) {	//服务器给发送过来了数据
			//读取套接字数据,处理
			bzero (buf, BUFSIZ);
			do {
				ret = read (fd, buf, BUFSIZ - 1);
			} while (ret < 0 && EINTR == errno);
			if (ret < 0) {
				perror ("read from socket");
				continue;
			}
			if (!ret)
				break;			/* 服务器关闭 */

			//There is a BUG,FIXME!!
			printf ("server said: %s\n", buf);
			if ((strlen(buf) > strlen(SERV_RESP_STR)) 
				&& !strncasecmp (buf+strlen(SERV_RESP_STR), QUIT_STR, strlen (QUIT_STR))) {	//用户输入了quit字符
				printf ("Sender Client is exiting!\n");
				break;
			}

		}
	}

	/*4.关闭套接字 */
	close (fd);
}

注脚


  1. 计算机网络中的“透明”性,主要表现在网络体系结构中层次关系的设计与利用两个方面:
    1)从设计者的角度看,
    “透明”性主要指各层的服务功能相对独立,具体实现的技术细节要尽可能的封装于本层之内,提供简化、规范、统一的调用接口。(即下层为上层提供透明调用)
    2)从使用者(用户)的角度看,“透明”性主要指上层调用下层服务时,不需要了解下层的技术细节(甚至不知道、或认为不存在),通过简化、规范、统一的调用接口实现对下层服务的调用。(即上层看下层是“透明”的)比如,数据链路层是通过“0比特插入/删除”方法来实现数据帧的透明传输的,上层无论给出何种比特序列,数据链路层都能以数据帧的方式传输。 ↩︎

  2. HTTP 是 Web 应用的应用层协议。定义浏览器如何向 Web 服务器发送请求以及 Web 服务器如何向浏览器
    进行响应。
    目前主要使用的 HTTP/1.0 和 HTTP/1.1,尤其以 HTTP/1.1 为主流。
    LV.6 网络编程_第45张图片LV.6 网络编程_第46张图片
    讲解:HTTP 作为 Web 应用的应用层协议,依据每次请求后是否需要重新建立一个连接,分为非持久
    连接和持久连接的 HTTP。因为非持久连接需要每次重新建立连接的弊端,提出了并行连接和持久连接的优
    化技术,但并行连接有并行 TCP 连接数的限制,故产生了持久连接的方式。又根据是否可以连续依次发送
    请求,分为非流水方式持久连接和流水方式持久连接。非流水方式必须等待一个对象请求并响应后才可以
    对下一个对象请求,流水方式可以连续依次发送请求,节省时间,故现在流行的 HTTP/1.1 默认使用流水方
    式持久连接。 ↩︎

  3. 进程间通信:
    1.进程间的数据共享:
    管道、消息队列、共享内存、unix域套接字
    易用性: 消息队列 > unix域套接字 >管道 > 共享内存(经常要和信号量一起用)
    效率: 共享内存 > unix域套接字 >管道 > 消息队列
    常用:共享内存、unix域套接字
    2.异步通信:
    信号
    3.同步和互斥(做资源保护)
    信号量 ↩︎

你可能感兴趣的:(嵌入式学习笔记,网络,tcp/ip,网络协议)