当前位置首页 > Linux知识

(54)LINUX应用编程和网络编程之九Linux网络通信实践

阅读次数:239 次  来源:admin  发布时间:

3.9.1.linux网络编程框架 3.9.1.1、网络是分层的 (1)OSI 7层模型(理论指导) (2)网络为什么要分层 (3)网络分层的具体表现 3.9.1.2、TCP/IP协议引入(网络分层实现的具体实现) (1)TCP/IP协议是用的最多的网络协议实现 (2)TCP/IP分为4层,对应OSI的7层 (3)我们编程时最关注【应用层】,了解传输层(TCP/UDP/TFTP),网际互联层和网络接入层不用管 3.9.1.3、BS和CS (1)CS架构介绍(client server, 客户端服务器架构) (2)BS架构介绍(broswer server,浏览器服务器架构) ----------------------------------------------------------------------------------------------------------------------------------------------------------- 网络编程补充:注意在不同函数中sockfd是不同的。 服务器端 (1)创建套接字函数:int socket(int domain,int type,int protocol);第一个参数是创建套接字所使用的协议栈,通常为AF_INET;di第二个参数是用来指定套接字的类型,即指定数据流套接字(SOCK_STREAM)还是数据报套接字(SOCK_DGRAM);参数protocol通常设置为0.返回一个socket描述符。 (2)绑定套接字:将套接字与计算机上的某个端口/IP绑定,进而在该端口监听服务请求,使用 int bind(int sockfd,struct sockaddr *my_addr,int addrlen),我们计算机存储数据的方式是低位字节优先,而数据在网络上传输的时候是高位字节优先,所以一般需要先进行字节转换。其中htonl()和htons()函数是把主机字节顺序转化为网络字节顺序;ntohl()函数和ntohs()是将网络字节顺序转化为主机字节顺序。 (3)监听网络端口请求:int listen(int sockfd,int backlog),backlog用来设置请求队列中允许的最大请求数。注意:TCP传输中为被动套接字设置了两个队列:完全建立连接的队列和未完全建立连接的队列。 (4)接收连接请求:从完全建立连接的队列中接收一个连接,使用int accept(int sockfd,void *addr,int *addrlen);参数addr为一个指向sockaddr_in结构体的指针,这个结构体是客户端的IP地址和端口。服务器接收客户端的连接请求后,accept函数会返回一个新的socket描述符,服务器进程可以使用这个新的描述符同客户进程传输数据。 (5)面向连接的数据传输:面向连接就是说在连接成功之后才会进行数据传输。 int send(int sockfd,const void *msg,int len,unsigned int flags);其中第一个参数为第四个步骤中产生的新的socket描述符;参数msg为一个指针,指向要发送的数据;参数len为要发送的数据长度;参数flag为控制选项,一般置0. int recv(int sockfd,void *buf,int len,unsigned int flags);其中第一个参数为第四个步骤中的socket描述符;参数buf为存放接收数据的缓冲区;参数len为缓冲的字节数;参数flags为控制选项,一般情况下设定为0,。 (6)关闭套接字:int close(int sockfd);其中参数为第四个步骤中产生的新的socket描述符 客户端 (1)创建套接字:int socket(int domain,int type,int protocol);第一个参数是创建套接字所使用的协议栈,通常为AF_INET;di第二个参数是用来指定套接字的类型,即指定数据流套接字(SOCK_STREAM)还是数据报套接字(SOCK_DGRAM);参数protocol通常设置为0.返回一个socket描述符。 (2)与服务器建立连接:int connect(int sockfd,struct sockaddr *serv_addr,int *addrlen );其中第一个参数为第一个步骤中的socket描述符,第二个参数是指向sockaddr结构的指针,该结构体中包含了要连接的服务器端的IP地址和端口信息。 (3)面向连接的数据传输:面向连接就是说在连接成功之后才会进行数据传输。 int send(int sockfd,const void *msg,int len,unsigned int flags);其中第一个参数为第一个步骤中的socket描述符;参数msg为一个指针,指向要发送的数据;参数len为要发送的数据长度;参数flag为控制选项,一般置0. int recv(int sockfd,void *buf,int len,unsigned int flags);其中第一个参数为第一个步骤中的socket描述符;参数buf为存放接收数据的缓冲区;参数len为缓冲的字节数;参数flags为控制选项,一般情况下设定为0。 (4)关闭套接字:int close(int sockfd);其中参数为第一个步骤中的socket描述符 服务器模型 (1)循环服务器模型和并发服务器模型:因为对于循环服务器来说,TCP循环服务器一次只能够处理一个客户端的请求,处理完成后才能够接受下一个请求,所以很少使用。我们经常使用的是并发服务器模型,所谓并发,也就是说这种服务器模型在同一个时刻可以响应多个客户端的请求。它的核心思想就是每一个客户端的请求并不是由服务器进程直接处理,而是由服务器创建一个子进程来处理,这个优势可以弥补TCP循环服务器容易出现某个客户端独占服务端的缺陷。 (2)TCP并发服务器的模型如下:

(3)并发服务器模型代码示例: #include<stdlib.h> #include<stdio.h> #include<unistd.h> #include<errno.h> #include<string.h> #include<sys/types.h> #include<netinet/in.h> #include<sys/socket.h> #include<arpa/inet.h> #define SERV_PORT 5550 //服务器监听端口号 #define LENGTH 10 //请求队列的长度 #define SIZE 128 //缓冲区的长度 int main() { int res; int pth; //定义创建子进程标识符 int sockfd; //定义监听sockfd描述符 int clientfd; //定义数据传输sockfd描述符 struct sockaddr_in hostaddr; //服务器(主机)IP地址和端口号信息 struct sockaddr_in clientaddr; //客户端IP地址和端口号信息 unsigned int addrlen; char buf[SIZE]; //定义缓冲区 int cnt; /*创建套接字*/ sockfd = socket(AF_INET,SOCK_STREAM,0); if(sockfd == -1) { printf("套接字创建失败!\n"); exit(1); } /*将套接字与IP地址和端口号进行绑定*/ hostaddr.sin_family = AF_INET; //TCP/IP协议 hostaddr.sin_port = htons(SERV_PORT); //让系统随机选择一个未被占用的端口号 hostaddr.sin_addr.s_addr = INADDR_ANY; //服务器(主机)IP地址 bzero(&(hostaddr.sin_zero),8); //清零 res = bind(sockfd,(struct sockaddr *)&hostaddr,sizeof(struct sockaddr) ); if(res == -1) { perror("套接字绑定失败!\n"); exit(1); } /*将套接字设置为监听模式,以等待客户端的连接请求*/ res = listen(sockfd,LENGTH); if(res == -1) { perror("设置监听模式失败!\n"); exit(1); } printf("等待客户端请求连接.......\n"); /*循环等待客户端的连接请求*/ while(1) { addrlen = sizeof(struct sockaddr_in); clientfd =accept(sockfd, (struct sockaddr *)&clientaddr, &addrlen); /*接受一个客户端连接*/ if(clientfd == -1) { perror("与客户端连接错误\n"); continue; } pth = fork(); //创建子进程 if(pth<0) { perror("子进程创建失败\n"); exit(1); } if(pth == 0) //子进程,处理客户端的请求 { // close(sockfd); //关闭父进程的套接字 printf("客户端IP:%s\n", inet_ntoa(clientaddr.sin_addr)); //输出客户端IP地址 cnt = recv (clientfd ,buf ,SIZE ,0); //接收客户端的数据 if(cnt==-1) { perror("接收客户端数据失败\n"); exit(1); } printf("收到的数据是: %s\n",buf); close(clientfd); //关闭当前客户端的连接 exit(0); //子进程退出 } close(clientfd); //父进程中,关闭子进程的套接字,准备接收下一个客户端的连接 } return 0; } (4)多路复用 I/O 并发服务器:为了解决子进程带来的系统资源消耗问题 ----------------------------------------------------------------------------------------------------------------------------------------------------------- 3.9.2.TCP协议的学习1 3.9.2.1、关于TCP理解的重点 (1)TCP协议工作在传输层,对上服务socket接口(操作系统跟网络编程有关的API),对下调用IP层(网络层) (2)TCP协议面向连接,通信前必须先3次握手(经过3个步骤的握手通信)建立连接关系后才能开始通信(打电话例子)。 qq聊天就是面向非连接的。 (3)TCP协议提供可靠传输,不怕【丢包】、【乱序】等。 3.9.2.2、TCP如何保证可靠传输 (1)TCP在传输有效信息前要求通信双方必须先握手,建立连接才能通信 (2)TCP的接收方收到数据包后会ack回应(每次通信都有回应)给发送方,若发送方未收到ack会丢包重传 (3)TCP的有效数据内容会附带校验码,以防止内容在传递过程中损坏 (4)TCP会根据网络带宽(网络带宽)来自动调节适配速率(滑动窗口技术)也就是说通信双方的通信速率不是固定的,通信速率和两个方面有关:(1)一个包的大小(2)发送包的频率 (5)发送方会给各分割报文编号,接收方会校验编号,一旦顺序错误即会重传。 3.9.3.TCP协议的学习2(参考http://blog.csdn.net/whuslei/article/details/6667471) 3.9.3.1、TCP建立连接时的三次握手 (1)建立连接需要三次握手 (2)建立连接的条件:服务器listen(监听:随时等待客户端的连接)时客户端主动发起connect 3.9.3.2、TCP关闭连接时的四次握手 (3)关闭连接需要四次握手 (4)服务器或者客户端都可以主动发起关闭 注:这些握手协议已经封装在TCP协议内部,socket编程接口平时不用管 3.9.3.3、基于TCP通信的服务模式 (1)具有公网IP地址(在外网可以访问)的服务器(或者使用动态IP地址映射技术:花生壳) (2)服务器端socket、bind、listen后处于监听状态 (3)客户端socket后,直接connect去发起连接。 (4)服务器收到并同意客户端接入后会建立TCP连接,然后双方开始收发数据,收发时是双向的,而且双方均可发起 (5)双方均可发起关闭连接 3.9.3.4、常见的使用了TCP传输协议的网络应用 (1)http、ftp(单纯的传文件) (2)QQ服务器 (3)mail服务器 3.9.4.socket编程接口介绍 3.9.4.1、建立连接 (1)socket。socket函数类似于open,用来打开一个网络连接,如果成功则返回一个网络文件描述符(int类型),之后我们操作这个网络连接都通过这个网络文件描述符。 /* #include <sys/socket.h> int socket(int domain, int type, int protocol); domain表示是IPV4还是IPV6.内部是一个宏定义,参数domain用来指定要创建的套接字所使用的协议栈,通常为AF_INET.表示互联网协议族(TCP/IP协议族) type:用来指定套接字的类型 (1) SOCK_STREAM TCP协议 数据流套接字 (2)SOCK_DGRAM UDP协议 数据报套接字 (3)SOCK_SEQPACKET protocol:通常设为0,让系统根据我们之前设定的domain和type自动设置协议。 返回值:一个socket描述符,是指向内部数据结构的指针;在调用过程中出现错误的时候会返回-1. */ (2)bind 绑定套接字函数 /* #include <sys/socket.h> int bind(int sockfd, const struct sockaddr *address,socklen_t address_len); */ sockfd为要绑定的socket描述符,address为一个指向本机IP地址和端口号等信息的sockaddr结构体的指针,address_len通常设置为sockaddr结构的长度。 返回值:函数调用成功后返回值为0,否则返回值为-1. sockaddr结构用来保存socket信息: struct sockaddr { unsigned short sa_family; //表示套接字的协议栈地址,对于TCP/IP可以设置为 AF_INET char sa_data[14]; //sa_data为套接字的IP地址和端口号 }; (3)listen // int listen(int socket, int backlog); backlog表示请求队列中允许的最大请求数。 (4)connect客户端 /* int connect(int socket, const struct sockaddr *address,socklen_t address_len);*/ 返回成功返回值为0,否则为-1. 3.9.4.3、建立连接后进行数据的发送和接收 (1)send和write /*ssize_t send(int socket, const void *buffer, size_t length, int flags);*/ flags:一般设置为0即可 (1)MSG_EOR:Terminates a record (if supported by the protocol). (2)MSG_OOB:Sends out-of-band data on sockets that support out-of-band communications. The significance and semantics of out-of-band data are protocol-specific. (2)recv和read /* ssize_t recv(int socket, void *buffer, size_t length, int flags);*/ 3.9.4.4、辅助性函数(主要是进行IP地址转换的) (1)inet_aton、inet_addr、inet_ntoa (2)inet_ntop(从32位二进制转换为点分十进制字符串)、inet_pton(从点分十进制字符串转换为32位二进制)(推荐使用) 注意:点分十进制字符串即:"192.168.1.11"类型 3.9.4.5、表示IP地址相关数据结构 (1)都定义在 netinet/in.h (2)struct sockaddr,这个结构体是网络编程接口中用来表示一个IP地址的,注意这个IP地址是不区分IPv4和IPv6的(或者说是兼容IPv4和IPv6的) (3)typedef uint32_t in_addr_t; 网络内部用来表示IP地址的类型(vi /usr/include/netinet/in.h) (4)struct in_addr { in_addr_t s_addr; }; (5)struct sockaddr_in 针对IPV4 { __SOCKADDR_COMMON (sin_); in_port_t sin_port; /* Port number. */ struct in_addr sin_addr; /* Internet address. */ /* Pad to size of `struct sockaddr'. */ unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE - sizeof (in_port_t) - sizeof (struct in_addr)]; }; (6)struct sockaddr 这个结构体是linux的网络编程接口中用来表示IP地址的标准结构体,bind、connect等函数中都需要这个结构体,这个结构体是兼容IPV4和IPV6的。在实际编程中这个结构体会被一个struct sockaddr_in或者一个struct sockaddr_in6所填充。 3.9.5.IP地址格式转换函数实践 补充: (一)大端模式,是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理地址由小向大增加,而数据从高位往低位放; 小端模式,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低,和我们的逻辑方法一致。 (二) 网络字节序:网络字节是只允许大段模式 电脑是小端模式:要转换为大端模式 3.9.5.1、inet_addr、inet_ntoa、inet_aton 3.9.5.2、inet_pton、inet_ntop inet_addr解析:会自动检测我们电脑是大端还是小端 /* #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> in_addr_t inet_addr(const char *cp); //参数是一个点分十进制的IP地址字符串,转为in_addr_t,即uint32_t网络字节大端格式的二进制 */ inet_pton解析:参数是一个点分十进制的IP地址字符串,转为32位二进制 兼容IPV6 /* #include <arpa/inet.h> int inet_pton(int af, const char *src, void *dst); */ inet_ntop 解析: /* #include <arpa/inet.h> const char *inet_ntop(int af, const void *src,char *dst, socklen_t size); */ 代码示例: #include <stdio.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define IPADDR "192.168.1.102" // 0x66 01 a8 c0 // 102 1 168 192 // 网络字节序,其实就是大端模式 int main(void) { struct in_addr addr = {0}; char buf[50] = {0}; addr.s_addr = 0x6703a8c0; inet_ntop(AF_INET, &addr, buf, sizeof(buf)); printf("ip addr = %s.\n", buf); /* 模块二 // 使用inet_pton来转换 int ret = 0; struct in_addr addr = {0}; ret = inet_pton(AF_INET, IPADDR, &addr); if (ret != 1) { printf("inet_pton error\n"); return -1; } printf("addr = 0x%x.\n", addr.s_addr); */ /*模块一 in_addr_t addr = 0; addr = inet_addr(IPADDR); printf("addr = 0x%x.\n", addr); // 0x6601a8c0 */ return 0; } 3.9.6_7.soekct实践编程1_2 3.9.6.1、服务器端程序编写 服务器跟你的这台电脑的IP地址相绑定。当客户端路由到你这台电脑的IP后,怎么找到你这台电脑的具体哪个服务器呢?答案就是通过端口号来标识。 IP地址用来精确到某一台电脑,端口号用来标识这台电脑的某一个具体进程。 (1)socket (2)bind (3)listen (4)accept,返回值是一个fd,accept正确返回就表示我们已经和前来连接我的客户端之间建立了一个TCP连接了,以后我们就要通过这个连接来和客户端进行读写操作,读写操作就需要一个fd,这个fd就由accept来返回了。 注意:socket返回的fd叫做监听fd,是用来监听客户端的,也就是判断是哪一个客户端连接来的,不能用来和任何客户端进行读写;accept返回的fd叫做连接fd,用来和连接那端的客户端程序进行读写。 3.9.6.2、客户端程序编写 (1)socket (2)connect 概念:端口号,实质就是一个数字编号,用来在我们一台主机中(主机的操作系统中)唯一的标识一个能上网的进程。端口号和IP地址一起会被打包到当前进程发出或者接收到的每一个数据包中。每一个数据包将来在网络上传递的时候,内部都包含了发送方和接收方的信息(就是IP地址和端口号),所以IP地址和端口号这两个往往是打包在一起不分家的。 3.9.8.socket实践编程3 3.9.8.1、客户端发送&服务器接收 3.9.8.2、服务器发送&客户端接收 3.9.8.3、探讨:如何让服务器和客户端好好沟通 (1)客户端和服务器原则上都可以任意的发和收,但是实际上双方必须配合:client发的时候server就收,而server发的时候client就收 (2)必须了解到的一点:client和server之间的通信是异步的,这就是问题的根源 (3)解决方案:依靠应用层协议来解决。说白了就是我们server和client事先做好一系列的通信约定。 3.9.9.socket编程实践4 3.9.9.1、自定义应用层协议第一步:规定发送和接收方法 (1)规定连接建立后由客户端主动向服务器发出1个请求数据包,然后服务器收到数据包后回复客户端一个回应数据包,这就是一个通信回合,完成了一次数据交换 (2)整个连接的通信就是由N多个回合组成的。 3.9.9.2、自定义应用层协议第二步:定义数据包格式 3.9.9.3、常用应用层协议:http、ftp······ 3.9.9.4、UDP简介
上一篇:Linux安装JDK
下一篇:linux下Clang和gcc的区别