嵌入式 网络编程1

目录

一、网络发展历史

1.1 ARPANET阶段

1.2 TCP/IP两个协议阶段

1.3 OSI开放系统互联模型阶段

1.4 TCP/IP协议族的体系结构

二、TCP和UDP的异同

三、网络基础知识

3.1 字节序

 3.2 socket

3.3 IP地址

3.4 端口号

四、TCP网络编程

4.1 网络编程模型

4.2 TCP网络编程流程

4.3 函数说明

4.3.1 socket函数

4.3.2 bind函数

4.3.3 listen函数

4.3.4 accept函数

4.3.5 connect函数

4.4 代码实现

4.4.1 服务器代码

4.4.2 客户端代码


一、网络发展历史

         ARPANET阶段

        TCP/IP两个协议阶段

        网络体系结构--OSI开放系统互联模型

        TCP/IP协议族体系结构

1.1 ARPANET阶段

         ARPAnet,即“阿帕网”,是Interne的最早雏形

        早期的ARPAnet使用网络控制协议(Network Control Protocol,NCP),

        不能互联不同类型的计算机和不同类型的操作系统,没有纠错功能

1.2 TCP/IP两个协议阶段

       TCP/IP协议分成了两个不同的协议:

       用来检测网络传输中差错的传输控制协议TCP

       专门负责对不同网络进行互联的互联网协议IP

1.3 OSI开放系统互联模型阶段

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

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

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

       OSI是由 ISO(国际标准化组织提出的)

共有七层:

     从下往上说也行,从上往下说也可以,但是顺序不能颠倒。

     物数网传会表应。

OSI模型相关的协议已经很少使用,但模型本身非常通用。

OSI模型是一个理想化的模型,尚未有完整的实现。

1.4 TCP/IP协议族的体系结构

        由于OSI开放系统互联模型太过于繁琐,所以目前还没有完整的实现。

        TCP/IP协议族体系结构相当于将OSI开放系统互联模型简化了。

        虽然是简化了层次,但是要做的事情还是一样的。

       TCP/IP协议族是一组协议,通常称它为TCP/IP协议族

       共有四个层次:链路层、网络层、传输层、应用层。

每层常见的协议:

        应用层:

               HTTP(Hypertext Transfer Protocol) 超文本传输协议

              万维网的数据通信的基础

              FTP(File Transfer Protocol) 文件传输协议

             是用于在网络上进行文件传输的一套标准协议,使用TCP传输

             TFTP(Trivial File Transfer Protocol) 简单文件传输协议

             是用于在网络上进行文件传输的一套标准协议,使用UDP传输

             SMTP(Simple Mail Transfer Protocol) 简单邮件传输协议

             一种提供可靠且有效的电子邮件传输的协议

       传输层:

            TCP(Transport Control Protocol) 传输控制协议

            是一种面向连接的、可靠的、基于字节流的传输层通信协议

            UDP(User Datagram Protocol) 用户数据报协议

           是一种无连接、不可靠、快速传输的传输层通信协议

      网络层:

            IP(Internetworking Protocol) 网际互连协议

           是指能够在多个不同网络间实现信息传输的协议

           ICMP(Internet Control Message Protocol) 互联网控制信息协议

           用于在IP主机、路由器之间传递控制消息,ping命令使用的就是该协议

           IGMP(Internet Group Management Protocol) 互联网组管理

           是一个组播协议,用于主机和组播路由器之间通信

     链路层:

           ARP(Address Resolution Protocol) 地址解析协议

           通过IP地址获取对方mac地址

           RARP(Reverse Address Resolution Protocol) 逆向地址解析协议

           通过mac地址获取对方ip地址

注意:每层使用的协议,有下层决定,不能乱用

 

数据封包和拆包的过程

 

一帧数据说明:

有的书上写的是 64-1518 ,相当于将以太网头(14字节)和以太网尾(4字节)算进去了

1500是最大传输单元,linux系统默认的是 1500,使用 ifconfig 命令查看 MTU后面的数值即可

二、TCP和UDP的异同

          共同点:同为传输层协议

          不同点:

                    TCP:有连接,可靠

                    UDP:无连接,不保证可靠

          TCP(即传输控制协议)概念:

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

                适用情况:

                    适合于对传输质量要求较高,以及传输大量数据的通信。

                    在需要可靠数据传输的场合,通常使用TCP协议

                    MSN/QQ等即时通讯软件的用户登录账户管理相关的功能通常采用TCP协议

        UDP(User Datagram Protocol)用户数据报协议

                是不可靠的无连接的协议。

                在数据发送前,因为不需要进行连接,所以可以进行高效率的数据传输。

           适用情况:

                发送小尺寸数据(如对DNS服务器进行IP地址查询时)

                在接收到数据,给出应答较困难的网络中使用UDP。(如:无线网络)

                适合于广播/组播式通信中。

               MSN/QQ/Skype等即时通讯软件的点对点文本通讯以及音视频通讯通常采用UDP协议

              流媒体、VOD、VoIP、IPTV等网络多媒体服务中通常采用UDP方式进行实时数据传输

三、网络基础知识

3.1 字节序

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

             小端序(little-endian) - 低序字节存储在低地址

             将低字节存储在起始地址,称为“Little-Endian”字节序,Intel、AMD等采用的是这种方式;

            大端序(big-endian)- 高序字节存储在低地址

            将高字节存储在起始地址,称为“Big-Endian”字节序,由ARM、Motorola等所采用

判断自己主机的字节序:

#include <stdio.h>
int main(){
    int a = 0x12345678;
    char *p = (char *)&a;
    if(*p == 0x12){
        printf("大端\n");
    }else if(*p == 0x78){
        printf("小端\n");
    }
    
    return 0;
}

       

如果多台主机在通信的过程中,如果字节序不一样,那么就有可能导致收到的数据和发送的不一致的情况,所以为了保证网络通信的过程中数据的一致性,就发明了 网络字节序(大端序),意味着发送方发送数据时,需要转换成网络字节序发送,接收方接到的数据就是网络字节序的数据,需要转换成主机字节序再处理。

什么样的数据收发的时候需要考虑字节序?

         明确知道通信的双发的主机字节序一样时,可以不用考虑

         将超过一个字节的数据作为整体收发的时候,需要考虑字节序(字符串不需要考虑)

字节序转换的方式,以4字节 主机字节序转网络字节序 为例:

#include <stdio.h>
int main(){
    int  num;
    scanf("%d", &num);
    int test_num = 0x12345678; 
    char *test = (char *)&test_num;
    if(*test == 0x12){
        num = num;
    }else if(*test == 0x78){
        char *p = (char *)&num;
        char *q = p+3;
        char temp = *p;
        *p = *q;
        *q = temp;
        p++;
        q--;
        temp = *p;
        *p = *q;
        *q = temp;
    }
    return 0;
}

字节序转换的函数:

        h host 主机          to 转换           n network 网络            l int         s short

uint32_t htonl(uint32_t hostlong); //主机字节序转网络字节序 4字节

uint16_t htons(uint16_t hostshort); //主机字节序转网络字节序 2字节

uint32_t ntohl(uint32_t netlong); //网络字节序转主机字节序 4字节

uint16_t ntohs(uint16_t netshort); //网络字节序转主机字节序 2字节

例如:

#include <stdio.h>
#include <arpa/inet.h>

int main(int argc, const char *argv[])
{
	unsigned int num = 0x12345678;
	unsigned int ret = htonl(num);

	printf("%#x --> %#x\n", num, ret);

	return 0;
}

执行结果:

 3.2 socket

       linux内核的五大功能:

                 进程管理 ----时间片轮转、上下文切换

                 文件管理 ----将一堆 0 和 1 转换方便人类识别的字符

                 设备管理 ----linux下一切皆文件

                 内存管理 ----内存的分配和回收

                 网络管理 ----网络协议栈的管理

socket是一个系统调用接口,会返回一个文件描述符,用户想通过网络收发数据的时候,只需要对该文件描述符进行读写即可,数据使用什么协议发送,通过调用socket函数时传参的方式来决定,相当于把复杂的网络通信的过程,转换成了我们熟悉的文件io操作。

socket并不仅限于TCP/IP协议族体系结构,其他的体系结构也会使用socket

socket也并不仅限于linux

socket的类型:

           流式套接字(SOCK_STREAM)

           提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复的发送且按发送顺序接               收。内设置流量控制,避免数据流淹没慢的接收方。数据被看作是字节流,无长度限制。               ----TCP

           数据报套接字(SOCK_DGRAM)

           提供无连接服务。数据包以独立数据包的形式被发送,不提供无差错保证,数据可能丢失               或重复,顺序发送,可能乱序接收。----UDP

          原始套接字(SOCK_RAW)

          可以对较低层次协议如IP、ICMP直接访问。

3.3 IP地址

MAC地址是网卡的物理地址,理论上讲应该是全球唯一的,

但是现在有很多虚拟化的技术,已经不保证全球唯一了,但是同一局域网中一定唯一。

MAC地址占用 6个字节,例如 00:0c:29:a1:f9:68

linux中使用 ifconfig 命令可以查看mac地址

win中使用 ipconfig/all 命令可以查看mac地址

局域网内部使用 MAC地址通信,如果数据想走出局域网,就需要用到IP地址了。

           IP地址的分类

                   IPV4 4字节 32bit

                   IPV6 16字节 128bit

现在企业中大部分使用的还是IPV4,因为有NAT技术的加持,

能很大程度缓解IP地址不够用的问题。

IP地址的表示形式 "192.168.80.10" 这种叫做点分十进制,是一个字符串

计算机中存储IP地址是用的无符号4字节整型。unsigned int

           IPV4地址的组成:

                     由 网络号 和 主机号 组成

         IPV4地址分类:

                 网络号 主机号    规定最高位       范围                                        使用单位

         A      1字节   3字节          0            0.0.0.0 - 127.255.255.255         政府/大公司/学校

        B       2字节   2字节        10          128.0.0.0-191.255.255.255         中等规模的公司

        C       3字节   1字节       110          192.0.0.0-223.255.255.255           个人

                                                              192.168.1.255 广播地址

        D                                    1110          [224-239]         组播

        E                                    11110         [240-255]        保留测试用的

其中每个IP地址又可以通过路由器,下发局域网IP地址,

每类IP地址都有专门划分子网的保留段。

子网掩码:

           是由一堆连续的1和连续的0组成的。

           用来和IP地址取与运算来获取网络号的。

           从而能限制某一网段内能容纳的最大主机数。

           如,ip地址是192.168.70.8 子网掩码设置成 255.255.255.0

               取与运算可以得到的结果:192.168.70.0 ----这是网络号,网络号相同时,才能进行同信

               这时,该网段内共有IP地址 256 个:

                        其中 192.168.70.0 是网络号,是不能占用的

                        192.168.70.255 是广播的地址,也不能占用

                        网关设备也需要占用一个IP地址,一般是同一局域网内可用的编号最小的。

             192.168.70.1

      所以能容纳的主机数:256-1-1-1(网关设备也可以算作一台IP主机) = 254

192.168.70.x 网段将子网掩码设置成 255.255.255.128,同一子网能容纳的最大主机数?

答案:

        这种方式可以将同一网段再划分成两个子网

        第一个子网IP地址范围:192.168.70.0~192.168.70.127

        第二个子网IP地址范围:192.168.70.128~192.168.70.255

 其中每个子网中有需要减掉 网络号 广播地址 网关地址

        所以子网掩码不一定都是255255255.0 网关设备的IP地址也不一定是编号最小的。

既然是无符号4字节整型,就涉及字节序的问题。

在网络字节序下,ip地址的存储:

//将点分十进制的字符串 转换成 网络字节序的 无符号四字节整型

in_addr_t inet_addr(const char *cp);

//将网络字节序的无符号四字节整型的ip地址 转换成 点分十进制的字符串

char *inet_ntoa(struct in_addr in);

例如:

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(int argc, const char *argv[])
{
	char str[] = "192.168.80.30";

	unsigned int ip = inet_addr(str);
	unsigned char *p = (unsigned char *)&ip;

	printf("%s  --> %d.%d.%d.%d\n", str, *p, *(p+1), *(p+2), *(p+3));

	return 0;
}

3.4 端口号

网络通信是不通主机的进程间通信,通过IP地址可以在复杂的网络环境中找到对应的主机

但是还得确定消息发给哪个进程。

如果使用pid来标识进程

          第一个问题pid没法手动指定

          第二个问题是进程重启时一般pid都会发生变化,不好管理

所以就发明了端口号,用来人为的标识某一个进程。

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

众所周知端口:1~1023(1~255之间为众所周知端口,256~1023端口通常由UNIX系统占用)

已登记端口:1024~49151

动态或私有端口:49152~65535

linux系统端口号的范围 [0-65535] 共计 65536个 使用 unsigned short 存储

实际开发的时候,端口号是由用户指定的,学习阶段我们为了防止冲突

可以使用一些特殊数字的,如 8888 9999 6789 6666 等

linux系统中 /etc/services 中保存的就是当前系统中已经被占用的端口号

常见的服务使用的端口号:

        ftp 21

        ssh 22

        tftp 69

        http 80 8080

四、TCP网络编程

4.1 网络编程模型

C/S模型 客户端/服务器模型

B/S模型 浏览器/服务器

4.2 TCP网络编程流程

服务器流程:

             创建套接字

             填充服务器的网络信息结构体

             将套接字和服务器的网络信息结构体绑定

             把套接字设置成被动监听状态

             阻塞等待客户端连接

             收发数据

             关闭套接字

  客户端流程:

             创建套接字

             填充服务器的网络信息结构体

             与服务器建立连接

             收发数据

             关闭套接字

4.3 函数说明

4.3.1 socket函数

功能:
	创建套接字
头文件:
	#include <sys/types.h>
	#include <sys/socket.h>
函数原型:
	int socket(int domain, int type, int protocol);
参数:
	domain:
		通信域
			AF_UNIX, AF_LOCAL	本地通信使用
			AF_INET				IPV4使用
			AF_INET6			IPV6使用
			AF_PACKET			原始套接字使用
	type:
		套接字类型
			SOCK_STREAM			TCP使用
			SOCK_DGRAM			UDP使用
			SOCK_RAW			原始套接字使用
	protocol:
		附加协议,如果没有  传0即可
	
返回值:
	成功	文件描述符
	失败	-1

4.3.2 bind函数

功能:
	绑定套接字和网络信息结构体
头文件:
	#include <sys/types.h>
	#include <sys/socket.h>
函数原型:
	int bind(int sockfd, const struct sockaddr *addr,
                socklen_t addrlen);
参数:
	sockfd:套接字
	addr:	网络信息结构体
			struct sockaddr {
               sa_family_t sa_family;
               char        sa_data[14];
			}
			上面的结构体只是为了防止编译警告 做强转用的
			我们用的是下面的结构体
			struct sockaddr_in {
				sa_family_t    sin_family; /* AF_INET */
				in_port_t      sin_port;   /* 网络字节序的端口号 */
				struct in_addr sin_addr;   /* 网络地址 */
			};

			/* Internet address. */
			struct in_addr {
				uint32_t       s_addr;     /* 网络字节序无符号4字节整型的IP地址 */
			};
	addrlen:addr
返回值:
	成功 0
	失败 -1

4.3.3 listen函数

功能:
	将套接字设置成被动监听状态
头文件:
	#include <sys/types.h>
	#include <sys/socket.h>
函数原型:
	int listen(int sockfd, int backlog);
参数:
	sockfd:套接字
	backlog:半监听队列的长度
			一般设置成  5  10 都可以 只要不是0 就行
返回值:
	成功 0
	失败 -1

4.3.4 accept函数

功能:
	阻塞等待客户端连接
头文件:
	#include <sys/socket.h>
函数原型:
	int accept(int socket, struct sockaddr *restrict address,
           socklen_t *restrict address_len);
参数:
	socket:已经设置成listen状态的套接字
	address:用来保存客户端网络信息结构体的缓冲区的首地址
			如果不想保存,可以设置成 NULL
	address_len:address  如果不需要 也可以传 NULL
返回值:
	成功 返回一个新的文件描述符专门用于和该客户端通信
	失败 -1

4.3.5 connect函数

功能:
	与服务器建立连接
头文件:
	#include <sys/types.h>
	#include <sys/socket.h>
函数原型:
	int connect(int sockfd, const struct sockaddr *addr,
                   socklen_t addrlen);
参数:
	sockfd:套接字
	addr:	服务器的网络信息结构体
	addrlen:addr大小
返回值:
	成功 0
	失败 -1

4.4 代码实现

4.4.1 服务器代码

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>

int main(int argc, const char *argv[])
{
	//创建套接字  --买了一部手机
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if(-1 == sockfd){
		perror("socket error");
		exit(-1);
	}

	//填充服务器网络信息结构体 --买卡
	struct sockaddr_in serveraddr;
	serveraddr.sin_family = AF_INET;
	//端口号可以自己指定 一般使用 8888 9999 等 都可以
	serveraddr.sin_port = htons(8889);
	//ip地址不能乱填 自己的Ubuntuip地址是多少就填多少
	//如果是自己的主机测试,不走网络的话 可以使用 127.0.0.1
	serveraddr.sin_addr.s_addr = inet_addr("192.168.80.58");

	socklen_t serveraddr_len = sizeof(serveraddr);
	
	//将套接字和服务器网络信息结构体绑定  --插卡
	if(-1 == bind(sockfd, (struct sockaddr *)&serveraddr, serveraddr_len)){
		perror("bind error");
		exit(-1);
	}

	//将套接字设置成被动监听状态
	if(-1 == listen(sockfd, 5)){
		perror("listen error");
		exit(-1);
	}

	//阻塞等待客户端连接
	//如果成功了, 对acceptfd的操作 就是对当前连接上的客户端的操作
	int acceptfd = accept(sockfd, NULL, NULL);
	if(-1 == acceptfd){
		perror("accept error");
		exit(-1);
	}
	printf("有客户端连接到服务器..\n");
	
	char buff[128] = {0};
	//接收数据
	read(acceptfd, buff, 128);
	printf("客户端发来数据[%s]\n", buff);
	//组装应答消息
	strcat(buff, "--hqyj");
	//发送应答
	write(acceptfd, buff, 128);

	//关闭套接字
	close(acceptfd);
	close(sockfd);

	return 0;
}

4.4.2 客户端代码

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>

int main(int argc, const char *argv[])
{
	//创建套接字
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if(-1 == sockfd){
		perror("socket error");
		exit(-1);
	}

	//填充服务器网络信息结构体 --用来指定连接哪个服务器
	struct sockaddr_in serveraddr;
	serveraddr.sin_family = AF_INET;
	//端口号可以自己指定 一般使用 8888 9999 等 都可以
	serveraddr.sin_port = htons(8889);
	//ip地址不能乱填 自己的Ubuntuip地址是多少就填多少
	//如果是自己的主机测试,不走网络的话 可以使用 127.0.0.1
	serveraddr.sin_addr.s_addr = inet_addr("192.168.80.58");

	socklen_t serveraddr_len = sizeof(serveraddr);
	
	//与服务器建立连接
	if(-1 == connect(sockfd, (struct sockaddr *)&serveraddr, serveraddr_len)){
		perror("connect error");
		exit(-1);
	}

	printf("与服务器连接成功..\n");
	
	char buff[128] = {0};
	fgets(buff, 128, stdin);
	buff[strlen(buff)-1] = '\0';

	//发送数据
	write(sockfd, buff, 128);
	//接收应答消息
	read(sockfd, buff, 128);
	printf("应答消息[%s]\n", buff);
	
	//关闭套接字
	close(sockfd);

	return 0;
}

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值