今天是6.2号,到6.14号需要提交《中期检查报告》、《学术报告》,现在论文需要大修,至少6.15中期答辩后再次提交,怎么也要在赶在6.24号入职之前定稿投出去。
先修改小论文、修改《中期检查报告》,再写《学术报告》,最后制作PPT,一件一件事情搞吧......痛苦啊......
抽着时间把这篇论文补一下吧...
几个名词的说明:
参考文献:
[1] 《嵌入式Linux开发教程(上册)》
[2] Linux Socket过程详细解释(包括三次握手建立连接,四次握手断开连接)
[3] TCP三次握手四次挥手详解
[4] bind:address already in use的深刻教训以及解决办法
[5] socket套接字详解(TCP与UDP)
OSI模型(Open System Interconnection Model,开放系统互联模型)是一个由国际标准化组织提出的概念模型,试图提供一个使各种不同的计算机和网络在世界范围内实现互联的标准协议。
物理层负责将最后的信息编码成电流脉冲或其他信号在网上传输。他有计算和网络介质之间的实际界面所组成,可定义电气信号、符号、线的状态和始终要求、数据编码以及数据传输用连接器。
如最常用的RS232规范就属于物理层,所有比物理层高的层都用过事先定义好的接口和它通信。
数据链路层通过物理网络链路提供数据传输。不同的数据链路层定义了不同的网络和协议特征。其中包括物理编址、网络拓扑结构、错误校验、数据帧序列以及流控。
数据链路层实际上由两部分组成:介质存取控制(Media Access Control,MAC)和逻辑链路控制(Logical Link COntrol,LLC)。
MAC描述在共享介质环境中如何进行调度、发送和接收数据。MAC确保信息跨链路的可靠传输,对数据传输进行同步,识别错误和控制数据的流向。
逻辑链路控制子层管理单一网络链路上设备间的通信。
网络层负责在源和终点之间建立连接,按一般包括网络寻径,还可能包括流量控制、错误检查等。
相同MAC标准不同网段之间的数据传输一般只涉及数据链路层。而不同MAC标准之间的数据传输都涉及到网络层。网络层使不同类型的数据网络能够实现互联。
传输层向高层提供可靠的端到端的网络数据流服务。传输层的功能一般包括流控、多路传输、虚电路管理以及差错检验和恢复。
会话层建立、管理和终止表示层和实体之间的通信会话。通信会话包括发生在不同网络应用层之间的服务请求和服务应答,这些请求与应答通过会话层的协议实现。他还包括创建检查点,是在通信发生中断的时候可以返回到以前的某个状态。
表示层提供多种功能用于应用层数据编码和转化,以确保一个系统应用层发送信息可以被另一个系统应用层识别。表示层的编码和转化包括公共数据表示格式、性能转化表示格式、公共数据压缩模式和公共数据加密模式。
表示层协议一般不与特殊的协议栈相关联,如QuickTime是Apple计算机视频和音频的标准,MPEG是ISO的视频压缩和编码标准。常见的图形图像格式如GIF、JPEG是不同静态图片压缩和编码标准。
应用层是最接近用户的OSI层,OSI应用层与用户之间是通过软件直接相互作用的。
OIS应用层协议包括文件的传输、访问以及管理协议(FTAM),以及文件虚拟终端协议(VIP)和公共管理系统信息(CMIP)等。
网络接口层包括用于协作IP数据在已有的网络介质上传输的协议。实际上TCP/IP标准并不定义与OSI数据链路层和物理层相对应的功能。相反,它定义像地址解析协议这样的协议,提供TCP/IP协议的数据结构和实际物理硬件之间的接口。
网络层对应OSI七层参考模型的网络层。本层包含IP协议、RIP协议,负责数据的包装、寻址和路由。同时还包含网间控制报文协议(ICMP),用来提供网络诊断信息。
传输层对应于OSI的传输层,提供两种端到端的通信服务。其中TCP协议提供可靠的数据流服务,UDP协议提供不可靠的用户数据流服务。
应用层对应OSI的应用层和表示层。因特网的应用层协议包括Finger,Whois、FTP(文件传输协议)、Gopher、HTTP(超文本出阿叔协议)、TELNET(远程终端协议)、SMTP(简单邮件传输协议)、IRC(因特网中断会话)、NNTP(网络新闻传输协议)等。
IP(Internet Protocol,网际协议)是网间层的主要协议,任务是在源地址和目的地之之间传输数据。IP协议只是尽最大努力来传输数据包,并不保证所有的包都可以传输到目的地,也不保证数据包的顺序和位移。
IP定义了TCP/IP的地址、寻址方法、以及路由的规则。现在广泛使用的IP协议有IPv4和IPv6。
在计算机中,多字节的对象都被表示为连续的字节序列,而存储在内部的排列有两个通用的规则,一个多位的整数将按照其存储地址的最低或最高字节排列。如果最低有效字节在最高有效字节的前面,则称为小端序,反之称为大端序。
比如对于0x12345678这样的32进制数,需要4个字节。
在网络应用中,字节序是必须要考虑的一个因素,因为不同机器类型可能采用不同标准的字节序,所以均需按照网络标准转化。网络传输的标准叫做网络字节序,实际上是大端序,而常用的X86或者ARM往往都是小端序,ARM的字节序实际上是可配置的,但一般都是配置为小端。
在网络编程中不应该假设自己的程序运行的主机字节序,应当使用htonl/htons/ntohs/ntohl之类的函数在网络字节序和主机字节序之间进行转换。其中h代表host,就是本地主机的表示形式。n代表network,表示网络上传输的字节序;s和l代表类型short和long。
网络上进行通信的各个端点,很多时候是遵循客户机/服务器模型的。
服务器端特征如下:
客户端的特征如下:
本地的进程间通信(IPC)有很多种方式,可以总结为下面4类:
消息传递(管道、FIFO、消息队列)
同步(互斥量、条件变量、读写锁、文件和写记录锁、信号量)
共享内存(匿名的和具名的)
远程过程调用(Solaris门和Sun RPC)
网络中进程之间如何通信?首要解决的问题是如何唯一标识一个进程,否则通信无从谈起。在本地可以通过进程PID来唯一标识一个进程,但是在网络中这是行不通的。其实TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。
用Socket能够实现网络上不同主机之间或同一主机不同对象之间的数据通信,所以,现在Socket已经是一类通信接口的集合。广分为网络Socket和本地Socket。
(1)本地Socket在Linux上包括UNIX Domain Socket和Netlink两种。UNIX Domain Socket主要用于进程间通信,Netlink则用于用户空间和内核空间通讯。
(2)网络Socket支持很多不同的协议,以下主要基于TCP/IP协议族中的TCP和UDP协议的网络编程。谈论仅限于IPv4网络的协议族和地址表示。
1. 地址表示数据结构
IP协议使用的地址描述数据结构在netinet/in.h中定义。
Linux下该结构的典型说明如下:
/* Structure describing an Internet (IP) socket address. */
#define __SOCK_SIZE__ 16 /* sizeof(struct sockaddr) */
struct sockaddr_in {
__kernel_sa_family_t sin_family; /* Address family */
__be16 sin_port; /* Port number 端口号 */
struct in_addr sin_addr; /* Internet address IP地址 */
/* Pad to size of `struct sockaddr'. */
unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
sizeof(unsigned short int) - sizeof(struct in_addr)];
};
sin_family该字段被赋值为AF_INET,表示IPv4协议族。
sin_port为端口号,通常1024号以下的端口需要root权限才可以使用。另外有很多已经约定好了对应特定服务的端口号,具体可以查看/etc/services,在选用自定义协议的端口号时,尽量不要和已知服务重合。
struct in_addr是在通信时使用的IP地址结构,Linux下的原型如下:
/* Internet address. */
typedef __u32 __bitwise __be32;
struct in_addr {
__be32 s_addr;
};
只要填充这个结构体,这是一个32位二进制整数代表的IP地址,对应一个本机有效网络接口的地址,也可以调重围INADRR_ANY,代表本机可以使用的网络地址。大部分时候都是用INADDR_ANY来填充此处。
一段典型的填充IP地址数据结构的代码如下:
...
struct sockaddr_in addr;
...
addr.sin_family = AF_INET; /* 使用IPv4协议族 */
addr.sin_port = htons(80); /* 设置端口号为80 */
addr.sin_addr.s_addr = inet_addr("192.168.0.1"); /* 设置IP地址为192.168.0.1 */
注意:
sin_port和sin_addr.s_addr两个值都是多字节的整数,Socket规定这里必须使用网络字节序。
2. 网络字节序和本地字节序之间的转换
有四个基本函数,可以实现网络字节序和本地字节序之间的转换。函数头文件、原型及函数功能说明如下:
#include
/* 32位整数从主机字节序转换为网络字节序 */
uint32_t htonl(uint32_t hostlong);
/* 16位整数从主机字节序转换为网络字节序 */
uint16_t htons(uint16_t hostshort);
/* 32位整数从网络字节序转换为主机字节序 */
uint32_t ntohl(uint32_t netlong);
/* 16位整数从网络字节序转换为主机字节序 */
uint16_t ntohs(uint16_t netshort);
3. 主机名和地址转换函数
在实际网络编程中,往往需要在IP地址的点分十进制表示和二进制表示之间相互转化,也需要进行主机名和地址转换,因此系统提供了一系列函数,一般需要包含头文件:
#include
#include
(1)in_addr_t inet_addr(const char* cp)
将一个点分十进制IP地址转换成in_addr_t类型,该类型实际上是一个32位无符号整数,实际上就是:
struct in_addr {
__be32 s_addr;
};
中s_addr域的数据类型。注意这个二进制表示的IP地址规定的是网络字节序。
192.168.0.1在PC上会被转换成0x0100A8C0
(2)char* inet_ntoa(struct in_addr in)
此函数可以将结构体struct in_addr中的二进制IP地址转换成一个点分十进制表示的字符串,返回这个字符串的首指针。他所返回的缓冲区是静态分配的,在并发或者异步使用时要小心,缓冲区可能随时被其它调用改写。
如下调用,会将一个网络字节序二进制无符号32位整数表示的IP地址0x0100A8C0转换为点分十进制表示"192.168.0.1":
...
char *str;
struct in_addr addr = {
s_addr = 0x0100A8C0,
}
...
str = inet_ntoa(addr);
...
(3)通过主机名获取IP地址
实际应用时,很多时候得到的通信另一方是主机名,所以需要将主机名转换为IP地址。传统上,有两个函数声明在netdb.h中进行这个操作。其中一个是gethostbyname()函数,其原型如下:
struct hostent *gethostbyname(const char* name);
直接根据主机名字符串返回一个struct hostent结构。此返回的数据结构有可能是静态分配的。
其中struct hostent在Linux下的原型如下:
socket即是一种特殊的文件,一些socket函数就是对其进行的操作 (读/写IO、打开、关闭)。下面介绍一些基本的socket操作函数。
在进行Socket通信之前,一般调用socket()函数来创建一个Socket通信端点。socket原型如下:
int socket(int domain, int type, int protocol);
socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。
参数说明:
注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。
函数成功返回一个有效的文件描述符,出错时返回-1。
调用socket()创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。
创建了Socket之后,可以调用bind()函数来讲这个Socket绑定到特定的地址和端口上进行通信,函数原型如下:
int bind(int socket, const struct sockaddr *address, socklent address_len);
参数说明如下:
函数调用成功时返回0,否则返回-1。
通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过这个地址来接连服务器;而客户端就不用指定,由系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
对于客户机,使用TCP协议时,在通信前必须调用connect()函数连接到服务器的特定通信端口才能正确进行通信。
对于使用UDP协议的客户机,这个步骤是可选项。如果使用了connect(),在此之后可以不需要指定数据报的目的地址而直接发送,否则每次发送数据均需要指定数据报的目的地址和端口。
connect()函数原型如下:
int connect(int socket, const sockaddr *address, socklent address_len);
其中所有参数的意义和bind()函数的参数一致。客户端通过调用connect函数来建立与TCP服务器的连接。
函数成功返回0,否则返回-1。
基于TCp协议的服务器,需要调用listen()函数将其socket设置成被动模式,等待客户机的链接。其函数原型如下:
int listen(int socket, int backlog);
参数说明:
socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。
函数调用成功返回0,失败返回-1。
TCP服务器还需要调用accept()来处理客户机的连接请求,函数原型如下:
int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);
参数说明:
函数成功返回一个有效的文件描述符,此文件描述符指向(成功与客户机建立链接可以进行数据交换的)Socket。服务器程序使用文件描述符来与客户端进行后续的交互。
(1)读数据函数
以下函数均可以读Socket数据:read()、recv()、recvfrom()、recvmsg()。函数原型分别如下:
#include
ssize_t read(int fd, void *buf, size_t count);
/*********************************************************/
#include
#include
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
(2)写数据函数
相应的write()、send()、sendto()和sendmsg()都可以发送数据到socket,功能和原型都类似于读数据函数,函数原型如下:
#include
ssize_t write(int fd, const void *buf, size_t count);
/****************************************/
#include
#include
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
三次握手(Three-way Handshake),是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。
三次握手的目的是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号并交换 TCP 窗口大小信息。在socket编程中,客户端执行connect()时。将触发三次握手。
tcp建立连接要进行“三次握手”,即交换三个分组。大致流程如下:
总结:客户端的connect( )在三次握手的第二次返回,而服务器端的accept( )在三次握手的第三次返回。
某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M;
另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;
一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N;
接收到这个FIN的源发送端TCP对它进行确认。
这样每个方向上都有一个FIN和ACK。
(1)服务器 server.c
#include /* See NOTES */
#include
#include
#include
#include
#include
#include
#include "mysocket.h"
int main(){
//create socket
int server_socket_fd = socket(AF_INET,SOCK_STREAM,0);
if(server_socket_fd < 0)
printf("create socket error!\n");
//bind
struct sockaddr_in server_addr,client_addr;
socklen_t serversock_len = sizeof(server_addr);
(void)memset(&server_addr,0,serversock_len);
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(PORT);
int on = 1;
if(setsockopt(server_socket_fd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(int)) < 0)
printf("setsockopt error!\n");
int bind_fd = bind(server_socket_fd,(const struct sockaddr*)&server_addr,sizeof(server_addr));
if(bind_fd < 0)
printf("bind error!\n");
//listen
if(listen(server_socket_fd,5) < 0)
printf("create listen error!\n");
printf("connect..\n");
//accept
// while(1){
printf("wait connect...\n");
int connect_sock_fd = accept(server_socket_fd,(struct sockaddr*)&client_addr,&serversock_len);
printf("connect success!\n");
//do something
int fork_fd = fork();
if(fork_fd < 0){
printf("fork error!\n");
close(server_socket_fd);
}
else if(fork_fd == 0){
printf("get siganel!\n");
printf("=============\n");
char buf[10];
if(read(connect_sock_fd,buf,sizeof(buf)/sizeof(buf[0])) < 0)
printf("read error!\n");
printf("recv data = %s\n",buf);
}
else{
wait(0);
// continue;
}
// printf("close socket!\n");
close(server_socket_fd);
// }
}
(2)客户机 client.c
#include /* See NOTES */
#include
#include
#include
#include
#include
#include
#include "mysocket.h"
#define SERVER_IP "192.168.0.3"
int main(int argc,char** argv)
{
struct sockaddr_in server_addr,client_addr;
/* if(argc !=2){
printf("usage: ./client \n");
exit(0);
}
*/
int client_socket_fd = socket(AF_INET,SOCK_STREAM,0);
if(client_socket_fd < 0)
printf("client socket create error!\n");
(void)memset(&client_addr,0,sizeof(client_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
if(bind(client_socket_fd,(const struct sockaddr*)&client_addr,(socklen_t)sizeof(struct sockaddr_in)) < 0)
printf("bind error!\n");
if(connect(client_socket_fd,(const struct sockaddr*)&server_addr,(socklen_t)sizeof(client_addr)) < 0)
printf("create connect error!\n");
printf("connect suceess!\n");
char send[6] = {'h','e','l','l','o','\0'};
int j = 5;
printf("count = %d\n",sizeof(send)/sizeof(send[0]));
printf("count!\n");
if(write(client_socket_fd,send,sizeof(send)/sizeof(send[0])+1) < 0)
printf("write error!\n");
printf("write success!\n");
}
#ifndef _MYSOCKET_H
#define _MYSOCKET_H
#include
#include
#define PORT 5555
#endif
(1)服务器
(2)客户机