Linux应用程序设计之网络基础编程
1、TCP/IP协议概述
1.1、OSI参考模型及TCP/IP参考模型
OSI协议参考模型是基于国际标准化组织(ISO)的建议发展起来的,从上到下工分为7层:应用层,表示层,会话层,传输层,网络层,数据链路层,物理层。与此相区别的TCP/IP协议模型一开始就遵循简单明确的设计思路,它将OSI的7层参考模型简化为4层,从而得到有利于实现和使用。TCP/IP协议参考模型和OSI协议参考模型的对应关系如下图所示:
网络接口层:负责将二进制流转换为数据帧,并进行数据帧的发送和接收。要注意的是数据帧是独立的网络星系传输单元。
网络层:负责将数据帧封装成IP数据报,并运行必要的路由算法。
传输层:负责端对端之间的通信会话连接和简历,传输协议的选择根据数据传输方式而定。
应用层:负责应用层序的网络访问,这里通过端口来识别各个不同的进程。
1.2、TCP/IP协议族
TCP/IP是一个庞大的协议族,它包括了各个层次上的众多协议,下图列举了各层只能怪一些重要的协议,并给出了协议在不同层次中处的位置:
ARP:用于获得同一物理网络中的硬件主机地址
MPLS:多协议标签协议。
IP:负责在主机和网络之间寻址和路由数据包。
ICMP:用于发送报告有关数据包的传送错误的协议;
IGMP:被IP主机用来想本地多路广播路由器报告主机组成员的协议;
TCP:为应用程序提供可靠的通信连接,适合于一次传输大批数据的情况,并适用于要求得到响应的应用程序。
UDP:提供了无连接通信,且不对传递包进行可靠的保证,适合于一次传输少量的数据,可靠性则由应用层来负责。
1.3、TCP和UDP
1、TCP
(1)概述
同其他特任何协议一样,TCP想相邻的高层提供服务,因为TCP的上一层就是应用层,因此,TCP数据传输实现了从一个应用程序到另一个应用程序的数据传递。应用程序通过编程调用TCP并使用TCP服务,提供需要准备发送的数据,用来区分接收数据应用的目的地址和端口号。
通常应用程序通过打开一个socket来使用TCP服务,TCP管理到其他socket的数据传递。可以说,通过IP的源/目的可以唯一的区分网络中两个设备的关联。
(2)三次握手协议
TCP对话通过三次握手来初始化的。三次握手的目的是使数据段的发送和接搜同步,告诉其他主机其一次可接搜的数据量,并建立虚链接。
三次握手的简单过程:
1、初始化主机通过一个同步标志置位的数据段发出会话请求;
2、接收主机通过发回具有以下项目的数据段表示回复:同步标志置位、即将发送的数据段的起始字节的顺序号、应答并带有将收到的下一个数据段的字节顺序号。
3、请求主机再回一个数据段,并带有确认顺序号和确认号。
流程示意图如下:
TCP实体所采用的基本协议是滑动窗口协议。当发送方传送一个数据报时,它将启动计时器。当该数据报到达目的地后,接收方的TCP实体向回发送一个数据报,其中包含一个确认序号,它的意思是希望收到下一个数据报的顺序号。如果发送方的定时器在确认信息到达之前超时,那么发送方会重发该数据报。
(3)TCP数据报头
TCP数据报头的含义如下:
源端口、目的端口:16位长。标识出远端和本地的端口号。
序号:32位长。标识发送的数据报的顺序。
确认号:32位长。希望收到的下一个数据报的序列号。
TCP头长:4位长。表明TCP头中包含多少个32位字。
6位未用。
ACK:ACK位置1表明确认号是合法的。如果ACK为0,那么数据报不包含确认信息,确认字段被省略。
PSH:表示是带有PUSH标志的数据,接收方因此请求数据报一到便可送往应用程序而不必等到缓冲区装满时才传送。
RST:用于复位由于主机崩溃或其他原因而出现的错误链接。还可以用于拒绝非法的数据报或拒绝连接请求。
SYN:用于建立连接
FIN:用于释放连接。
窗口大小:16位长。窗口大小字段表示在确认了字节之后还可以发送多少个字节。
校验和:16位长。是为了确保高可靠性而设置的。它的检验头部、数据伪TCP头部之和。
可选项:0个或多个32位字。包括最大TCP载荷,窗口比例、选择重发数据报等选项。
2、UDP
UDP协议并不需要建立一个明确的连接,因此建立UDP应用要比建立TCP应用简单得多。
UDP数据包头如下图所示:
源地址、目的地址、:16位长。标识远端和本地的端口号。
数据报的长度是指包括报头和数据部分在内的总的字节数,因为报头的长度是固定的。所以该域主要用来计算可变长度的数据部分(又称数据负载)
3、协议的选择
协议的选择要考虑三个方面:
(1)对数据可靠性的要求
对数据要求高可靠性的应用需要选择TCP协议,如验证、密码字段的传送都是不允许出错的,而对数据的可靠性要求不那么高的应用科选择UDP传送。
(2)应用的实时性
由于TCP协议在传送过程中要进行三次握手、重传确认等手段来保证数据传输的可靠性。使用TCP协议会有较大的延时。因此不适合对实时性较高的应用。如VOIP、视频监控等。相反,UDP协议则在这些应用中能发挥很好的作用。
(3)网络的可靠性
在网络状况不是很好的情况下需选用TCP协议(如广域网等情况),但是若在网络状况很好的情况下(如局域网)选择UDP协议来减少网络负荷。
4、网络基础编程
4.1、socket概述
1、socket定义
在Linux中的网络编程都是通过stocket接口来完成的。socket接口是一种特殊的I/O接口,它也是一种文件描述符。每一个socket都用一个半相关描述{协议,本地地址,本地端口}来表示;一个完整的套接字则用一个相关描述{协议,本地地址、本地端口、远程地址、远程端口}。socket也有一个类似打开文件的函数调用,该函数返回一个整型的socket描述符,随后的连接、数据传输等操作都是通过socket来实现的。
2、socket类型
常见的socket类型有3种:
(1)流式socket(SOCK_STREAM)
流式套接字提供可靠的、面向连接的通信流;它使用TCP协议,从而保证了数据传输的正确性和顺序性。
(2)数据报socket(SOCK_DGRAM)
数据报套接字定义了一种无连接的服务,数据通过相互独立的报文传输,是无序的。并且不保证是可靠的、无差错的。它使用数据报协议UDP。
(3)原始socket
原始套接字允许对底层协议如IP或IGMP进行直接访问,它功能强大但使用较为不便,主要用于一些协议的开发。
2.2、地址及顺序处理
1、地址结构相关处理
(1)数据结构介绍
sockaddr和sockaddr_in两个结构体用来保存socket的信息,如下所示: struct sockaddr { unsigned short sa_family;/*地址族*/ char sa_data[14];/*14字节的协议地址,包含该socket的IP地址和端口*/ }; struct sockaddr_in { short nt sa_family;/*地址族*/ unsigned short int sin_port;/*端口号*/ struct in_addr sin_addr;/*IP地址*/ unsigned char sin_zero[8];/*填充0以保持与struct sockaddr同样大小*/ };
(2)结构字段
结构头文件 | #include |
sa_family | AF_INET:IPv4协议 |
AF_INET6:IPv6协议 | |
AF_LOCAL:UNIX域协议 | |
AF_LINK:链路地址协议 | |
AF_KEY:密钥套接字(socket) |
2、数据存储优先顺序
(1)函数说明
计算机数据存储有两种字节优先顺序:高位字节优先和低位字节优先。Internet上数据以高位字节优先顺序在网络上传输,因此在有些情况下,需要对这这两个字节存储优先吮吸进行相互转化。四个函数:htons、ntohs、htonl、ntohl。
(2)函数格式
所需头文件 |
#include
|
函数原型 |
uint16_t htons(uint16_t host16bit)
uint32_t htonl(uint16_t host32bit)
uint16_t ntohs(uint16_t net16bit)
uint16_t ntohl(uint16_t net32bit)
|
参数 | host16bit:主机字节序的16bit数据 |
host32bit:主机字节序的32bit数据 | |
net16bit:网络字节序的16bit数据
|
|
net32bit:网络字节序的32bit数据 | |
返回值 | 成功:返回要转换的字节 |
失败:-1 |
3、地址格式化转化
(1)函数说明
通常用户在表达地址时采用点分十进制的数值。而通常使用的socket编程中使用的则是二进制,这就需要将这两个数值进行转换。这里IPv4用到的函数有:inet_aton、inet_addr、和inet_ntoa,而IPv4和IPv6兼容的函数有inet_pton(点分十进制地址映射为二进制地址)和inet_ntop(二进制地址映射为点分十进制地址)。
(2)函数格式
inet_pton函数:
所需头文件 | #include |
函数原型 | int inet_pton(int family,const char *strptr, void *addrptr) |
参数 | family: AF_INET:IPv4协议;AF_INET6:IPv6协议 |
strptr:要转化的值 | |
addrptr:转化后的地址 | |
返回值 | 成功:0 |
失败:-1 |
inet_ntop函数:
所需头文件 | #include |
函数原型 | int inet_ntop(int family,void *addrptr, char *strptr, size_t len) |
参数 | family: AF_INET:IPv4协议;AF_INET6:IPv6协议 |
strptr:要转化的值 | |
addrptr:转化后的地址;len:转化后值得大小 | |
返回值 | 成功:0 |
失败:-1 |
4、名字地址转化:
(1)函数说明
在Linux中,有一些函数可以实现主机名和地址的转化,最为常见的有:gethostbyname(主机名转化为IP)、gethostbyaddr(IP地址转化无主机名)、getaddrinfo(自动识别IPv4地址和IPv6地址)等。
gethostbyname(主机名转化为IP)和gethostbyaddr(IP地址转化无主机名)都涉及到一个hostent结构体:
struct hostent { char *h_name;/*正式主机名*/ char **h_aliases;/*主机别名*/ int h_addrtype;/*地址类型*/ int h_length;/*地址长度*/ char **h_addr_list;/*指向IPv4或IPv6的地址指针数组*/ };调用该函数后就能返回hostent结构体的相关信息。 getaddrinfo函数涉及到一个addrinfo结构体,如下: struct addrinfo{ int ai_flags;/*AI_PASSIVE,A_CANONNAME*/ int ai_family;/*地址族*/ int ai_socktype;/*socket类型*/ int ai_protocol;/*协议类型*/ size_t ai_addrlen;/*地址长度*/ char *ai_canoname;/*主机名*/ struct sockaddr *ai_addr;/socket结构体/ struct addrinfo *ai_next;/*下一个指针链表*/ }
gethostbyname函数:
所需头文件 | #include |
函数原型 | struct hostent *gethostbyname(const char *hostname) |
参数 | hostname :主机名 |
返回值 | 成功:hostent类型指针 |
失败:-1 |
调用该函数时可以先对addrinfo结构体中的h_addrtype和h_length进行设置若为Ipv4可设置为AF_INET和4,若为IPv6可设置为AF_INET6和16,如不设置则默认为IPv4地址类型。
getaddrinfo函数:
头文件 | #include |
函数原型 | int getaddringo(const *hostname,const char *service,const struct addrinfo *hints,struct addrinfo **result) |
参数 | hostname:主机名 |
service:服务名或十进制的串口字符串 | |
hints:服务线索 | |
result:返回结果 | |
返回值 | 成功:0 |
失败:-1 |
在调用前,首先对hints服务线索进行设置。它是一个addrinfo结构体
addrinfo结构体常见选项值
结构体头文件 | |
ai_flags | AI_PASSIVE:该套接口是用作被动的打开 |
AI_CANONNAME:通知getaddrinfo函数返回主机名字 | |
family | AF_INET:IPv协议 |
AF_INET6:IPv6协议 | |
AF_UNSPE:IPv4或IPv6均可: | |
ai_socktype | SOCK_STREAM:字节流套接字socket(TCP) |
SOCK_DGRAM:数据报套接字socket(UDP)
|
|
ai_protocol | IPPROTO_IP:IP协议 |
IPPROTO_IPV4:IPv4协议 | |
IPPROTO_IPV6:IPv6协议 | |
IPPROTO_UDP:UDP协议 | |
IPPROTO_TCP:TCP协议 |
(3)使用实例:
/*getaddrinfo.c*/ #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <netdb.h> #include <sys/types.h> #include <netinet/in.h> #include <sys/socket.h> int main() { struct addrinfo hints, *res = NULL; int rc; memset(&hints,0,sizeof(hints)); /*设置addrinfo结构体各参数*/ hints.ai_family = PF_UNSPEC; hints.ai_socktype = SOCK_DGRAM; hints.ai_protocol = IPPROTO_UDP; /*调用getaddrinfo函数*/ rc = getaddrinfo("127.0.0.1","123",&hints,&res); if(rc != 0) { perror("getaddrinfo"); exit(1); } else printf("getaddrinfo success\n"); }
4.3、socket基础编程
(1)函数说明
进行socket编程的基本函数有socket、bind、listen、accept、send、sendto、recv、recvform这几个;
socket:该函数用于建立一个socket连接,可指定socket类型等信息,在建立socket连接之后可对sockaddr或sockaddr_in进行初始化,以保存所建立的socket信息。
bind:该函数用于将本地IP地址绑定端口号,若绑定其他地址则不成功,另外,它主要用于TCP的连接,而在UDP中的连接中无必要。
connect:该函数在TCP中是用于bind的之后的client端,用于与服务器建立连接,而在UDP中由于没有了bind函数,因此connect有点类似bind函数的作用。
send和recv:这两个函数用于接收和发送数据,可一再TCP中,也可以在UDP中。当用在UDP时,可以在connect函数建立连接之后再用。
sendto和recvfrom:这两个函数的作用同send和recv函数类似,。当用在TCP时后面几个与地址有关的参数起不了作用。函作用等同于send和recv;当用在UDP时,可以在之前没有使用connect的情况时,这两个函数可以自动寻找指定地址进行连接。
服务器和客服端使用TCP协议的流程如如下图:
服务器和客户端使用UDP协议的流程图如下图:
(2)函数格式
socket函数
所需头文件 | #include | |
函数原型 | int socket(int family, int type, int protocol) | |
参数 |
family:
协议族
|
AF_INET:IPv4协议 |
AF_INET6:IPv6协议 | ||
AF_LOCAL:UNIX域协议 | ||
AF_ROUTE:路由套接字 | ||
AF_KEY:密钥套接字 | ||
type:
套接字类型
|
SOCK_STREAM:字节流套接字 | |
SOCK_DGRAM:数据报套接字 | ||
SOCK_RAW:原始套接字 | ||
protocol:0(原始套接字除外) | ||
返回值 | 成功:非负套接字描述符 | |
失败:-1 |
bind函数:
所需头文件 | #include |
函数原型 | int bind(int sockfd,struct sockaddr *my_addr, int addrlen) |
参数原型 | sockfd:套接字描述符 |
my_addr:本地地址 | |
addrlen:地址长度 | |
返回值 | 成功:0 |
失败:-1 |
端口号和地址在my_addr中给出了,若不指定地址,则内核随意分配一个临时端口给该应用程序。
listen函数:
所需头文件 | #include |
函数原型 | int listen(int socket, int backlog) |
参数原型 | sockfd:套接字描述符 |
backlog:请求队列中允许的最大请求数,大多数系统缺省值为20 | |
返回值 | 成功:0 |
失败:-1 |
accept函数:
所需头文件 | #include |
函数原型 | int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen) |
参数 | sockfd:套接字描述符 |
addr:客户端地址 | |
addrlen:地址长度 | |
返回值 | 成功:0 |
失败:-1 |
connect函数:
所需头文件 | #include |
函数原型 | int connect(int sockfd,struct sockaddr *serv_addr,int addrlen) |
参数 | sockfd:套接字描述符 |
serv_add:服务器地址 | |
addrlen:地址长度 | |
返回值 | 成功:0 |
失败:-1 |
send函数:
所需头文件 | #include |
函数原型 | int send(int sockfd, const void *msg, int len, int flags) |
参数 | sockfd:套接字描述符 |
msg:指向发送数据的指针 | |
len:数据长度 | |
flags:一般为0 | |
返回值 | 成功:发送的字节数 |
失败:-1 |
recv函数:
所需头文件 | #include |
函数原型 | int recv(int sockfd, void *buf, int len, unsigned int flags) |
参数 |
sockfd:套接字 描述符
|
buf:存放接收数据的缓冲区 | |
len:数据长度 | |
flags:一般为0 | |
返回值 | 成功:接收的字节数 |
失败:-1 |
sendo函数:
所需头文件 | #include |
函数原型 | int sendto(int sockfd, const void *msg,int len ,unsigned int flags,const struct sockaddr *to, int tolen) |
参数 | soctfd:套接字描述符 |
msg:指向要发送数据的指针 | |
len:数据长度 | |
flags:一般为0 | |
to:目的机的IP地址和端口信息 | |
tolen:地址长度 | |
返回值 | 成功:发送的字节数 |
失败:-1 |
recvfrom函数:
所需头文件 | #include |
函数原型 | int recvfrom(int sockfd, void *buf,int len, unsigned int flags,struct sockaddr *from,int *fromlen) |
参数 | sockfd:套接字描述符 |
buf:存放接收数据的缓冲区 | |
len:数据长度 | |
flags:一般为0 | |
from:源机的IP地址和端口号信息 | |
fromto:地址长度 | |
返回值 | 成功:接收的字节数 |
失败:-1 |
(3)使用实例
/*server.c*/ #include <sys/types.h> #include <sys/socket.h> #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <unistd.h> #include <netinet/in.h> #define SERVPORT 3333 #define BACKLOG 10 #define MAX_CONNECTED_NO 10 #define MAXDATASIZE 5 int main() { struct sockaddr_in server_sockaddr,client_sockaddr; int sin_size,recvbytes; int sockfd,client_fd; char buf[MAXDATASIZE]; /*建立socket连接*/ if((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1){ perror("socket"); exit(1); } printf("socket success! sockfd = %d\n", sockfd); /*设置sockaddr_in结构体相关参数*/ server_sockaddr.sin_family = AF_INET; server_sockaddr.sin_port = htons(SERVPORT); server_sockaddr.sin_addr.s_addr = INADDR_ANY; bzero(&(server_sockaddr.sin_zero),8); /*绑定函数bind*/ if(bind(sockfd,(struct sockaddr *)&server_sockaddr,sizeof(struct sockaddr)) == -1) { perror("bind"); exit(1); } printf("bind success!\n"); /*调用listen函数*/ if(listen(sockfd,BACKLOG) == -1) { perror("listen"); exit(1); } printf("listening...\n"); /*调用accept函数*/ if((client_fd = accept(sockfd,(struct sockaddr *)&client_sockaddr,&sin_size)) == -1) { perror("accept"); exit(1); } /*调用recv函数接收客户端的请求*/ if((recvbytes = recv(client_fd,buf,MAXDATASIZE,0)) == -1) { perror("recv"); exit(1); } printf("received a connection : %s\n",buf); close(sockfd); }
/*client.c*/ #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <netdb.h> #include <sys/types.h> #include <netinet/in.h> #include <sys/socket.h> #define SERVPORT 3333 #define MAXDATASIZE 100 int main(int argc,char **argv) { int sockfd,sendbytes; char buf[MAXDATASIZE]; struct hostent *host; struct sockaddr_in serv_addr; if(argc < 2) { fprintf(stderr,"Please enter the sercer's hostname!\n"); exit(1); } /*地址解析函数*/ if((host = (struct hostent *)gethostname(argv[1]))==NULL){ perror("gethostname"); exit(1); } /*创建socket*/ if((sockfd = socket(AF_INET, SOCK_STREAM,0)) == -1) { perror("socket"); exit(1); } /*设置sockaddr_in结构体相关参数*/ serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(SERVPORT); serv_addr.sin_addr=*((struct in_addr *)host->h_addr); bzero(&(serv_addr.sin_zero),8); /*调用connect函数主动发起对服务端的连接*/ if(connect(sockfd,(struct sockaddr *)&serv_addr,sizeof(struct sockaddr)) == -1) { perror("connect"); exit(1); } /*发送消息给服务器*/ if((sendbytes = send(sockfd,"hello",5,0)) == -1){ perror("send"); exit(1); } close(sockfd); }