-
网络编程部分课程安排:
01网络编程第一天
day01
- 了解OSI七层、TCP/IP四层网络模型结构
- 了解常见的网络网络协议格式
- 掌握网络字节序和主机字节序之间的转换(大端法和小端法)
- 说出TCP服务器端的通信流程
- 输出TCP客户端通信流程
- 独立写出TCP服务器端代码
- 独立写出TCP客户端代码
1.1 了解OSI七层、TCP/IP四层网络模型结构
- 协议:从应用程序的角度触发,协议可理解为数据传输和数据解释的规则(各个主机之间进行通讯所使用的共同语言)。
*文件传输的例子 -FTP
- OSI七层模型(理想)
路由器是连接不同网段的桥梁,网络层汇总有ip;TCP–socke(我们编),会话层是内核实现的,表示层:编解码,例如GBK,utf-8.
开发应用层协议
[]“物数网传会表应”
物理层 数据层 网络层 传输层 会话层 表示层 应用层
- 在网络过程中使用的模型
1.2 数据通信过程
数据通信过程是发送方层层打包的过程,接收方层层解包的过程。
1.3 网络应用程序常见的的设计模式
C/S模式:客户端和服务器端
B/S模式:浏览器和服务器模式
:优缺点:
对于C/S模式,可以保证性能,将数据缓存到客户端本地,提高数据传输效率,所用协议相对灵活。(传统的大型网络程序)
缺点:安全性,工作量。
B/S:浏览器不用开发,开发量小,移植性好,不受平台限制。
协议智能选择http协议,不能缓存数据,效率受影响。
1.4 网络常用的协议
- 以太网帧:;是包装在网络接口层(数据链路层:数据帧格式)的协议。(写代码用不到,可按位截取,str,memcpy)
判断数据类型,作用。ARP:ip->mac地址,RARP:mac->ip
- 例如帧格式写为0806的ARP解析的过程:
广播,匹配后发送ARP应答。 - ip协议 掌握如何拆分数据
- TCP和UDP的数据帧格式
TCP:(面向连接的) UDP面向无连接的。
32位序号,保证数据多线程的拼接。32为确认序号保证数据传输(安全)16位校验和(信号有可能被干扰后可能被篡改,用以保证)-》丢包重传功能,防篡改功能。ACK SYN(三次握手的请求标识) FIN(关闭请求),16位窗口大小(进行流量控制,防止接胡搜段和发送端速度不一致的情况)
其中有:8位生存时间ttl,(64)下一跳:为了防止网络堵塞。
-
UDP的数据报格式: (端口(用来区分应用程序):21 ftp ,22 ssh,telnet (远程登录)23)红色字体掌握。
1.5 网络名词扫盲
- 路由:从源地址到目标地址所经过的路径
路由器位于七层模型的第三层(网络层),作用是接收网络数据包
(作用:可以路由表和转发)
路由和交换的区别:
路由与交换之间的主要区别是发生在OSI参考模型第二层(数据链路层),而路由发生在第三层,即网络层。这一区别决定了路由交换在移动信息的过程汇总需要使用不同的控制信息。
- 路由表
是存储在路由器或者联网计算机中的电子表格或者类数据库。 - 缺省路由条目: 按照缺省路由条目规定的接口发送到下一跳地址。
- 以太网交换机工作原理:以太网交换机位于OSI网络参考模型的第二层(数据链路层),是一种基于MAC地址识别,完成以太网数据帧转发的网络设备。
- hub的工作原理;集线器,区别在于集线器能够提供更多的端口服务,也叫做多扣中继器。
- DNS服务器(Domain Name System)的缩写,是因特网的一项核心服务,它可以将域名和ip地址相互映射,能够使人更方便的访问互联网,不用记住ip地址串。(由解析器和域名服务器组成)
- 局域网(LAN):
覆盖地理范围小,只在一个相对独立的局部范围内联
使用专门铺设的传输介质进行联网,数据传输效率高(10Mb/s~10Gb/s)
通信延迟时间段,可靠性较高
局域网可以支持多种传输介质 - 广域网(WAN):不同局域网的远程计算机网,如互联网为最大的广域网。
- 端口:逻辑意义上的端口,一般是指TCP/IP协议的端口,端口号范围从0到65535,例如用于浏览网页服务的80端口,用于FTP服务的21端口等,端口号小于256的定义为常用端口。
- MTU:通信属于Maximum Transmission Unit,MTU),是指一种通信协议的所能通过最大的数据包大小,这个参数通常与网络接口有关。
以下是一些协议的MTU:
FDDI协议;4352字节。
以太网(Ethernet):1500字节
PPPoE(ADSL协议):1492字节;
X.25协议:576字节。
Point-to-Point:4470字节
1.6 重要的面试题
1.6.1 TCP如何建立连接. 另外:更详细的问题.
1.6.2 [TCP如何通信]
1.6.3 TCP如何关闭连接
1.6.4 什么是滑动窗口
差错控制和流量控制。
TCP中采用滑动窗口来进行传输控制,滑动窗口的大小意味着接收方还有多大的缓冲区可以用于接收数据。发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。
1.6.5 什么是半关闭
由于在全关闭的状态下,当服务器端的数据发送完毕要关闭连接的时候,这时客户端会接收到服务器端的请求,但由于使用全关闭状态,则客户端向服务器端发送的断开连接确认请求将无法再次返回给服务器端。此时就需要用到半关闭状态。为了让数据之间完全交换,也为了让客户端和服务器端之间的数据得到可控,而防止阻塞的情况发生,因此应该用shutdown函数在服务器端发送完数据之后,应该附带一个EOF,让客户端知道服务器端的数据已经发送完成。
为了保证数据的完全交换,应该留出足够长的连接时间,但是应该留出多长的时间呢?
比如客户端连接到服务器,服务器将一个文件传输给客户端,客户端收到后发送确认数据给服务器端
这时服务器端只需要连续的传输文件数据,而客户端却无法知道需要接收数据到何时,客户端也不可能无休止的调用输入函数,因为这有可能导致程序阻塞(调用的函数未返回)
服务器端应该在数据发送完毕后传递EOF表示文件结束,客户端接收到EOF即停止接收数据并向服务器端发送确认数据。close函数与shutdown都可以向客户端发送EOF数据,但使用close发送EOF后也无法接收对方传输的数据了,所以使用shutdown
1.6.6 局域网上的两台主机如何进行通信
通信方法???.
1.6.7 internet上的两台主机如何进行通信
1.6.8 如何在internet上唯一识别一个进程
1.6.9 为什么说TCP是可靠的连接,UDP不可靠
10.路由器和交换机的区别
11.点到点,端到端
2. SOCKET(套接字)编程
一个标注很全的网络服务器编程的例子:python:https://www.cnblogs.com/python58/p/10426188.html.
要让不同机器通信,不仅需要内核IPC,也需要sockt。
两个连接,如下为socket pair,一个文件名包括两个缓冲区,通信是双方的。(发对收),可以半关闭,例如只发不收。
<font color=>站在TCP/UDP协议开发,SOCKET编程,建立文件描述符,对文件描述符进行操作。
socket编程的主要的API函数:
int blind()
int listen()
int accept()
int connect() 客户端主动调用服务器
ssize_t read()
ssize_t write()
ssize_t recv()
ssize_t send()
对应recv和send这两个函数的标志位直接填0就可以了。
注意:如果写缓冲器已满,write也会阻塞,read读操作的时候,若读缓冲区没有数据会引起阻塞。’
2.1 socket编程预备知识
2.1 网络字节序转换
端口号的转换:
大端和小端的概念
大端:也叫高端字节序,也叫网络字节序,是低位地址存放高位数据,高位地址存放低位数据
小端:也叫低端字节序,是低地址存放低位数据,高地址存放高位数据
-
大端和小端的使用场合???
(1)超过一个字节的,例如int等。
(2)网络传输使用大端法,如果机器使用的是小端,则需要进行大小端的转换。下面4个函数是进行大小端转换的函数:
函数名h表示主机host, n表示网络network, s表示 short, l表示long。
#include<arga/inet>
uint32_t htonl(uint32_t hostlong);
uint32_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint32_t netshort);
手工计算转换结果:
2.2字节序和ip地址的转换函数
ip地址转换的函数:
点分十进制ip转换为16进制字节序
int inet_pton(int af,const char *src,vodi *dst) //将点分的十进制IP转换为大端模式的网络IP(整型4字节数)
af : AF_INET或 AF_INET6,传入的src最后保存到dst中
例如inrt_pton(AF_INET,"127.0.0.1",&serv.sinaddr.s)
2.3 socket编程常用的重要结构体
strcut sockaddr 很多网络编程函数诞生早于IPv4协议,那时候都使用的是sockaddr结构体,为了向前兼容,现在sockaddr退化成了(void *)的作用,传递一个地址给函数,至于这个函数是sockaddr_in还是sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。
struct sockaddr {
sa_family_t sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
};
//-------------------------------------
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)];
};
struct in_addr { /* Internet address. */
__be32 s_addr;
};
//------------------------------------------------------
struct sockaddr_in6 {
unsigned short int sin6_family; /* AF_INET6 */
__be16 sin6_port; /* Transport layer port # */
__be32 sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
__u32 sin6_scope_id; /* scope id (new in RFC2553) */
};
//------------------------------------------------------
struct in6_addr {
union {
__u8 u6_addr8[16];
__be16 u6_addr16[8];
__be32 u6_addr32[4];
} in6_u;
#define s6_addr in6_u.u6_addr8
#define s6_addr16 in6_u.u6_addr16
#define s6_addr32 in6_u.u6_addr32
};
#define UNIX_PATH_MAX 108
struct sockaddr_un {
__kernel_sa_family_t sun_family; /* AF_UNIX */
char sun_path[UNIX_PATH_MAX]; /* pathname */
};
TCP的通信编码的过程。
2.4.1 socket函数
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
domain:
AF_INET 这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址
AF_INET6 与上面类似,不过是来用IPv6的地址
AF_UNIX 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用
type:
SOCK_STREAM 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类型,这个socket是使用TCP来进行传输。
SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。
SOCK_SEQPACKET该协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的接受才能进行读取。
SOCK_RAW socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使用该协议)
SOCK_RDM 这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数据包的顺序
protocol:
传0 表示使用默认协议。
返回值:
成功:返回指向新创建的socket的文件描述符,失败:返回-1,设置errno
socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用read/write在网络上收发数据,如果socket()调用出错则返回-1。对于IPv4,domain参数指定为AF_INET。对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议。如果是UDP协议,则type参数指定为SOCK_DGRAM,表示面向数据报的传输协议。protocol参数的介绍从略,指定为0即可.
2.4.2 bind函数
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:
socket文件描述符
addr:
构造出IP地址加端口号
addrlen:
sizeof(addr)长度
返回值:
成功返回0,失败返回-1, 设置errno
服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络地址和端口号。
bind()的作用是将参数sockfd和addr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号。前面讲过,struct sockaddr *是一个通用指针类型,addr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度。如:
bind()的作用是将参数sockfd和addr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号。前面讲过,struct sockaddr *是一个通用指针类型,addr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度。如:
struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(6666);
首先将整个结构体清零,然后设置地址类型为AF_INET,网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址,端口号为6666。
2.4.3 listen函数
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
sockfd:
socket文件描述符
backlog:
排队建立3次握手队列和刚刚建立3次握手队列的链接数
典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的accept()返回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未accept的客户端就处于连接等待状态,listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接待状态,如果接收到更多的连接请求就忽略。listen()成功返回0,失败返回-1。
2.4.4 accept函数
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockdf:
socket文件描述符
addr:
传出参数,返回链接客户端地址信息,含IP地址和端口号
addrlen:
传入传出参数(值-结果),传入sizeof(addr)大小,函数返回时返回真正接收到地址结构体的大小
返回值:
成功返回一个新的socket文件描述符,用于和客户端通信,失败返回-1,设置errno
三方握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。addr是一个传出参数,accept()返回时传出客户端的地址和端口号。addrlen参数是一个传入传出参数(value-result argument),传入的是调用者提供的缓冲区addr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给addr参数传NULL,表示不关心客户端的地址。
我们服务器的程序结构如下:
while (1) {
cliaddr_len = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
n = read(connfd, buf, MAXLINE);
......
close(connfd);
}
整个是一个while死循环,每次循环处理一个客户端连接。由于cliaddr_len是传入传出参数,每次调用accept()之前应该重新赋初值。accept()的参数listenfd是先前的监听文件描述符,而accept()的返回值是另外一个文件描述符connfd(通信文件描述符),之后与客户端之间就通过这个connfd通讯,最后关闭connfd断开连接,而不关闭listenfd,再次回到循环开头listenfd仍然用作accept的参数。accept()成功返回一个文件描述符,出错返回-1。
2.4.5 connect函数
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockdf:
socket文件描述符
addr:
传入参数,指定服务器端地址信息,含IP地址和端口号
addrlen:
传入参数,传入sizeof(addr)大小
返回值:
成功返回0,失败返回-1,设置errno
客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。connect()成功返回0,出错返回-1。
2.5 在linux下运行cpp代码
2.6 服务器端开发流程(视频14)
- serve.c
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<ctype.h>
int main()
{
//1.创建socket,返回一个文件描述符 lfd----socket
int lfd = socket(AF_INET,SOCK_STREAM,0);
if(lfd<0)
{
perror("socket error");
return -1;
}
//2.将lfd和Ip PORT进行绑定 -----bind()
//int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
struct sockaddr_in serv; //复制方便
bzero(&serv,sizeof(serv));
serv.sin_family = AF_INET;
serv.sin_port = htons(8888); //网络字节序转换,不使用1024以前的端口
serv.sin_addr.s_addr = htonl(INADDR_ANY); //表示使用本地任意可用ip inet_pton
int ret = bind(lfd,(struct sockaddr *)&serv,sizeof(serv));
if(ret<0)
{
perror("bind error");
return -1;
}
//3.监听
// int listen(int socketfd,int backlog);
listen(lfd,128);
//int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
//3.接收时不关注客户端的ip地址
//int cfd = accept(lfd,NULL,NULL); //如果不关心,先填NULL
//printf("lfd==[%d],cfd==[%d]\n",lfd,cfd);
//3。接收时关注ip地址-----------------接收得到的IP地址
struct sockaddr_in client;
socklen_t len = sizeof(client);
int cfd = accept(lfd,(struct sockaddr *)&client, &len);
//打印出端口号和ip地址
char sIP[16];
memset(sIP,0x00,sizeof(sIP));
printf("client-->IP:[%s],PORT:[%d]\n",inet_ntop(AF_INET,&client.sin_addr.s_addr,sIP,sizeof(sIP)),ntohs(client.sin_port));
//-----------------------------------------------------
int n = 0;
int i = 0;
char buf[128];
while(1)
{
//4.读数据
memset(buf,0x00,sizeof(buf));
n = read(cfd,buf,sizeof(buf));
if(n<=0)
{
printf("read error or client close");
break;
}
printf("n==[%d],buf==[%s]\n",n,buf);
for(i=0; i<n; i++)
{
buf[i] = toupper(buf[i]); //大小写转换的功能
}
//5.发送数据:
write(cfd,buf,n);
}
//6.关闭监听文件描述符和通信文件描述符
close(lfd);
close(cfd);
}
2.7 客户端代码编写(视频15)
//查看连接状态:
netstat -anp | grep 8888
- client.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
int main()
{
//1.创建socket --通信文件描述符
int cfd = socket(AF_INET,SOCK_STREAM, 0);
if(cfd<0)
{
perror("socket error");
return -1;
}
//2.连接网络端
//int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
struct sockaddr_in serv;
serv.sin_family = AF_INET;
serv.sin_port = htons(8888);
inet_pton(AF_INET,"127.0.0.1",&serv.sin_addr.s_addr); //将ip地址也转化为网络字节序,也要高速对应的通信为iPv4还是ipv6
printf("[%x]",serv.sin_addr.s_addr);
int ret = connect(cfd,(struct sockaddr *)&serv,sizeof(serv));//建立连接后可以进行读数据
if(ret<0)
{
perror("connect error");
return -1;
}
int n = 0;
char buf[256];
while(1)
{
//3. 发送数据 发送数据的设备一定为网卡,驱动底层发出信息
memset(buf, 0x00, sizeof(buf));
n = read(STDIN_FILENO,buf, sizeof(buf)); //从标准输入读入信息
write(cfd, buf, n); //4.读服务端发来的数据
memset(buf, 0x00,sizeof(buf));
n = read(cfd, buf, sizeof(buf));
if(n<=0)
{
printf("read error or server closed,n==[%d]",n);
break;
}
printf("n==[%d], buf==[%s]\n",n, buf);
}
//关闭套接字
close(cfd);
return 0;
}