网络是由若干结点和连接这些结点的链路组成,网络中的结点可以是计算机,交换机、
路由器等设备。
把多个网络连接起来就构成了互联网。目前最大的互联网就是因特网。
网络设备有:交换机、路由器、集线器
传输介质有:双绞线、同轴电缆、光纤,无线
IP 地址就是给因特网上的每一个主机(或路由器)的每一个接口分配的一个在全世界
范围内唯一的标识符。IP 地址因其特殊的结构使我们可以在因特网上很方便地进行寻址。
IP 地址有分 IPV4 和 IPV6 两种类别格式,IPV4 是类似”A.B.C.D”的格式,它是 32 位的,用“.”分成四个段,每个段是 8 个位(值为 0-255),用 10 进制表示。IPV6 地址是 128 位,格式类似”XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX”,用“:“分成 8 个段,每个段 16 个位,用 4 个 16 进制数表示。
接下来描述的 IP 地址默认都指的是 IPV4 的地址。为了便于寻址,了解目标主机的位置,每个 IP 地址由网络号和主机号两个部分构成。同一个物理网络上所有的主机都使用同 一个网络号,只是主机号不同
计算机使用的是无符号整型,而用户熟悉的是点分十进制的字符串。
该函数可以将点分十进制字符串表示的ipv4地址转化为用网络字节序整数表示的ipv4.即无符号整型
win+r --->cmd-->ipconfig
ifconfig
应用程序的代号,进程号是会变的,但是端口号不会变。所以我们使用端口号去唯一标识应用程序的。
127.0.0.1是回送地址,指本地机,一般用来测试使用。回送地址(127.x.x.x)是本机回送地址(Loopback Address),即主机IP堆栈内部的IP地址,主要用于网络软件测试以及本地机进程间通信,无论什么程序,一旦使用回送地址发送数据,协议软件立即返回,不进行任何网络传输。
通信需要:源ip+端口 目的ip+端口
网络协议就是一组网络规则的集合,是我们共同遵守的约定或标准。常见的协议:
◼ HTTP:超文本传输协议
◼ FTP: 文件传输协议
◼ TELNET : 是 internet 远程登陆服务的标准协议。
◼ TCP : 传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可
靠的、基于字节流的传输层通信协议
◼ UDP :用户数据报协议
◼ IP : Internet Protocol 简称 IP,又译为网际协议或互联网协议
◼ ICMP :因特网控制报文协议
◼ ARP : 地址解析协议,是根据 IP 地址获取 MAC 地址的协议
◼ RARP : 逆地址解析协议
这里的ACK,FIN,SYN后面介绍
1.9.0.1 OSI/ISO模型
应用层
表示层
会话层
传输层 加TCP/IP协议
网络层 加IP协议
数据链路层 加帧
物理层
1.9.0.2 TCP/IP模型
应用层
表示层
网络层
网际接口层(数据链路层+物理层)
int socket(int domain, int type, int protocol)
有了套接字就可以通过网络进行数据的收发
socket()创建套接字,成功返回套接字的文件描述符,失败返回-1
domain: 设置套接字的协议族, AF_INET(IPv4)和 AF_INET6(IPv6)
type: 设置套接字的服务类型 SOCK_STREAM(流服务,用于TCP协议)和SOCK_DGRAM(数据报,用于UDP协议)
注意 PF开头的是协议族,AF开头的是地址族,在Windows上没有任何区别,在Linux上差别不大
protocol: 一般设置为 0,表示使用默认协议
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
用来指定套接字使用的IP地址和端口。
bind()将 sockfd 与一个 socket 地址绑定,成功返回 0,失败返回-1
sockfd 是网络套接字描述符,即socket()的返回值。
addr 是地址结构,是一个结构体。
addrlen 是 socket 地址的长度。
专用 socket 地址结构
TCP/IP 协议族有 sockaddr_in 和 sockaddr_in6 两个专用 socket 地址结构体,它们分
别用于 IPV4 和 IPV6:
sin_family: 地址族 AF_INET
sin_port: 端口号,需要用网络字节序表示,在这里我们要注意大小端问题,在不同的操作系统上,大小端是不一样的,为了方便我们的服务器端,我们可以使用htons()函数将所有端口的字节序统一好。
sin_addr: IPV4 地址结构:s_addr 以网络字节序表示 IPV4 地址
struct sockaddr_in
{
sa_family_t sin_family;
u_int16_t sin_port;
struct in_addr sin_addr;
};
socket被命名后,还不能马上接受客户连接,我们需要使用如下系统调用创建一个监听队列以存放待处理的客户连接。
int listen(int sockfd, int backlog);
listen()创建一个监听队列以存储待处理的客户连接,成功返回 0,失败返回-1
sockfd 是被监听的 socket 套接字
backlog 表示处于完全连接状态的 socket 的上限,典型值为5
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept()从 listen 监听队列中接收一个连接,成功返回一个新的连接 socket,
该 socket 唯一地标识了被接收的这个连接,失败返回-1
sockfd 是执行过 listen 系统调用的监听 socket
addr 参数用来获取被接受连接的远端 socket 地址,一般是客户端
addrlen 指定该 socket 地址的长度
注意:accept返回值为0表明我们的客户端结束标准输入,这时我们接受到的值就为0.
ssize_t recv(int sockfd, void *buff, size_t len, int flags);
ssize_t send(int sockfd, const void *buff, size_t len, int flags);
recv()读取 sockfd 上的数据,buff 和 len 参数分别指定读缓冲区的位置和大小
send()往 socket 上写入数据,buff 和 len 参数分别指定写缓冲区的位置和数据长度
flags 参数为数据收发提供了额外的控制,一般为0
关闭该连接对应的socket
int close(int sockfd);
close()关闭一个连接,实际上就是关闭该连接对应的 socket
与服务器端一样
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
connect()客户端需要通过此系统调用来主动与服务器建立连接,
成功返回 0,失败返回-1
sockfd 参数是由 socket()返回的一个 socket。
serv_addr 是服务器监听的 socket 地址
addrlen 则指定这个地址的长度
与服务器端一样
与服务器端一样
与服务器端一样
综上所述,TCP协议的基本流程就是如下图所示
1.0版本 只能进行一次通信
#include
#include
#include
#include
#include
#include
#include
int main()
{
int sockfd=socket(AF_INET,SOCK_STREAM,0);
if(sockfd==-1){
exit(1);
}
struct sockaddr_in saddr,caddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family=AF_INET;
saddr.sin_port=htons(6000);
saddr.sin_addr.s_addr=inet_addr("127.0.0.1");//当前主机的ip地址,通过ifconfig获取
int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
if(res==-1)
{
printf("bind err\n");
exit(0);
}
res=listen(sockfd,5);
if(res<0)
{
printf("listen err\n");
exit(0);
}
while(1)
{
int len=sizeof(caddr);
int c=accept(sockfd,(struct sockaddr*)&caddr,&len);
if(c<0)
{
continue;
}
printf("accept c=%d\n",c);
char buff[128]={0};
int n=recv(c,buff,strlen(buff)-1,0);
printf("n=%d\n",n);
printf("buff=%s\n",buff);
send(c,"ok",2,0);
close(c);
}
}
#include
#include
#include
#include
#include
#include
#include
int main()
{
int sockfd=socket(AF_INET,SOCK_STREAM,0);
if(sockfd==-1)
{
printf("socket err\n");
exit(1);
}
struct sockaddr_in saddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family=AF_INET;
saddr.sin_port=htons(6000);
saddr.sin_addr.s_addr=inet_addr("127.0.0.1");//注意服务器和客户端必须要连接在同一个网络下
int res=connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
if(res==-1)
{
printf("connect err\n");
exit(0);
}
printf("input:\n");
char buff[128]={0};
fgets(buff,128,stdin);
send(sockfd,buff,strlen(buff)-1,0);
memset(buff,0,128);
recv(sockfd,buff,127,0);
printf("recv:%s\n",buff);
close(sockfd);
}
结果
服务器端可以accept到客户端发送来的数据
客户端可以将服务器端接收到数据后返回的信息接受
TCP 协议提供的是:面向连接、可靠的、字节流服务。
使用 TCP 协议通信的双发必须先建立连接,然后才能开始数据的读写。双方都必须为该连接分配必要的内核资源,以管理连接的状态和连接上数据的传输。TCP 连接是全双工的,双方的数据可以通过一个连接进行读写。完成数据交换之后,通信双方都必须断开连接以释放系统资源。
TCP 的服务器端和客户端编程流程如下:
socket()方法是用来创建一个套接字,有了套接字就可以通过网络进行数据的收发。
bind()方法是用来指定套接字使用的 IP 地址和端口。
listen()方法是用来创建监听队列。监听队列有两种,一个是存放未完成三次握手的连接,一种是存放已完成三次握手的连接。listen()第二个参数就是指定已完成三次握手队列的长度。
accept()处理存放在 listen 创建的已完成三次握手的队列中的连接。每处理一个连接,则accept()返回该连接对应的套接字描述符。如果该队列为空,则 accept 阻塞。
connect()方法一般由客户端程序执行,需要指定连接的服务器端的 IP 地址和端口。该方法执行后,会进行三次握手, 建立连接。
send()方法用来向 TCP 连接的对端发送数据。send()执行成功,只能说明将数据成功写入到发送端的发送缓冲区中,并不能说明数据已经发送到了对端。send()的返回值为实际写入到发送缓冲区中的数据长度。
recv()方法用来接收 TCP 连接的对端发送来的数据。recv()从本端的接收缓冲区中读取数据,如果接收缓冲区中没有数据,则 recv()方法会阻塞。返回值是实际读到的字节数,如果recv()返回值为 0, 说明对方已经关闭了 TCP 连接。
close()方法用来关闭 TCP 连接。此时,会进行四次挥手
ACK:表示确认号是否有效,我们称携带ACK标志的TCP的报文段为确认报文段。
SYN:表示请求建立一个连接,我们称携带SYN标志的TCP的报文段为同步报文段。
FIN:表示通知对方本端要关闭连接了,我们称携带FIN标志的TCP的报文段为结束报文段。
seq(Sequence Number):表示这个tcp
包的序列号。
(1)第一次握手,主机A向主机B发出请求数据包:“我想给你发数据,可以吗?”这是第一次对话。
(2)第二次握手,主机B向主机A发送同意连接,并要求同步的数据包(同步就是两台主机协调工作,一台在发送,一台在接收):“可以,你什么时候发?”这是第二次对话。
(3)第三次握手,主机A再发出一个数据包确认主机B的要求同步:“我现在就发,你接收吧!”这是第三次对话。
主机A向主机B发送请求数据包,这是第一次对话,S表示SYN,即请求建立一个连接
主机B向主机A 发送同意连接(回应 ACK,确认该序列号有效),并要求同步数据包(SYN),所以有seq和ACK
主机A向主机B 发送一个数据包确认主机B的要求同步。ACK表示确认该序列号有效
A主机给B主机发送了一个长度为3的字符串
B主机给A主机发送了一个长度为2的字符串,表明他收到该信息
A主机给B主机发送了确认收到的,即ack
断开连接
四次挥手:
三次挥手:客户端和服务器端关闭相差不大,这个时候就只有三次即中间的ACK和FIN合并在一起。
服务器端和客户端可以进行多次交互,直到客户端结束连接
#include
#include
#include
#include
#include
#include
#include
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
{
exit(1);
}
struct sockaddr_in saddr, caddr;
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));
if (res == -1)
{
printf("bind err\n");
exit(0);
}
res = listen(sockfd, 5);
if (res < 0)
{
printf("listen err\n");
exit(0);
}
while (1)
{
int len = sizeof(caddr);
int c = accept(sockfd, (struct sockaddr *)&caddr, &len);
if (c < 0)
{
continue;
}
while (1)
{
printf("accept c=%d\n", c);
char buff[128] = {0};
int n = recv(c, buff, 127, 0);
if (n <= 0)//recv返回值n==0表明客户端已经关闭连接了。
{
break;
}
printf("n=%d\n", n);
printf("buff=%s\n", buff);
send(c, "ok", 2, 0);
}
sleep(2);
close(c);
}
}
如果将recv接受数据能力改成每次收取1个,即int n = recv(c, buff, 1, 0);
2.6.1 缓冲区
如果recv能力改成1,那么每次只能从缓冲区中读取一个字符,直到接收缓冲区中没有数据,然后才会阻塞
2.6.2 查看缓冲区
netstat -natp
Recv-Q :接收缓冲区
Send-Q:发送缓冲区
ESTABLLTSHED:已完成三次握手
此时有2个ok在接收缓冲区,在下一次输出
2.6.3 流失服务特点
不管中间如何分割,但是最后从发送端到接收端得到的结果是一致的
可靠性:
1:应答确认,超时重传机制
2:去重(报文有序号,有相同序号会丢失重复的),乱序重排(报文有序号)
2.6.4 粘包
粘包发生在发送或接收缓冲区中;应用程序从缓冲区中取数据是整个缓冲区中有多少取多少;那么就有可能第一个数据的尾部和第二个数据的头部同时存在缓冲区,而TCP是流式的,数据无边界,这时发生粘包。
客户端发了三次数据,服务器端一次接受完,客户端在Recv()处等待服务器回应收到,处于阻塞状态,而服务器端处于第二次recv()所以阻塞,这时这个粘包就会出现问题。解决方法就是在头部加上标识信息。
2.6.5 流量控制
TCP 协议是利用滑动窗口实现流量控制的。一般来说,我们总是希望数据传输得更快一些,不会一次只发一个字节。但是如果发送方把数据发得过快,接受方就可能来不及接收,这就会造成数据的丢失。所谓流量控制就是让发送方的发送速率不要太快,要让接收方来的及接收。
在 TCP 的报头中有一个字段叫做接收通告窗口,这个字段由接收端填充,是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。所以发送端就会有一个发送窗口,这个发送窗口的大小是由接收端填充的接收通告窗口的大小决定的,并且窗口的位置会随着发送端数据的发送和接收到接收端对数据的确认而不断的向右滑动,将之称为滑动窗口。发送方的滑动窗口示意图如下:
#include
#include
#include
#include
#include
#include
#include
int main()
{
int sockfd=socket(AF_INET,SOCK_STREAM,0);
if(sockfd==-1)
{
printf("socket err\n");
exit(1);
}
struct sockaddr_in saddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family=AF_INET;
saddr.sin_port=htons(6000);
saddr.sin_addr.s_addr= inet_addr("127.0.0.1");
int res=connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
if(res==-1)
{
printf("connect err\n");
exit(0);
}
while(1){
printf("input:\n");
char buff[128]={0};
fgets(buff,128,stdin);
if(strncmp(buff,"end",3)==0)
{
break;
}
send(sockfd,buff,strlen(buff)-1,0);
memset(buff,0,128);
recv(sockfd,buff,127,0);
printf("recv:%s\n",buff);
}
close(sockfd);
}
之前的代码存在一个问题:当一个客户端与服务器建立连接以后,服务器端 accept()返回,进而准备循环接收客户端发过来的数据。如果客户端暂时没发数据,服务端会在第 40 行的 recv()阻塞。此时,其他客户端向服务器发起连接后,由于服务器阻塞了,无法执行 accept()接受连接,也就是其他客户端发送的数据,服务器无法读取。服务器也就无法并发同时处理多个客户端。
目的:一个服务器能够同时收到多个客户端的数据
#include
#include
#include
#include
#include
#include
#include
#include
void * fun(void* arg)
{
int *p = (int*)arg;
int c = *p;
free(p);
while( 1 )
{
char buff[128] = {0};
int n = recv(c,buff,1,0);
if ( n <= 0)// recv返回值n == 0 说明客户但关闭连接了
{
break;
}
printf("recv=%s\n",buff);
send(c,"ok",2,0);
}
close(c);//关闭连接 挥手
printf("client close\n");
}
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);//套接字 文件描述符
if ( sockfd == -1 )
{
exit(1);
}
struct sockaddr_in saddr,caddr;//套接字地址 ip port
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("192.168.84.248");
int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
if ( res == -1 )
{
printf("bind err\n");
exit(1);
}
res = listen(sockfd,5);
if ( res == -1 )
{
exit(1);
}
while( 1 )
{
int c = accept(sockfd,NULL,NULL);
if ( c < 0 )
{
continue;
}
printf("accept c=%d\n",c);
pthread_t id;
int * p = (int*)malloc(sizeof(c));
*p = c;
pthread_create(&id,NULL,fun,(void*)p);
}
}
使用fork(),父子进程
#include
#include
#include
#include
#include
#include
#include
void fun(int c)
{
while( 1 )
{
char buff[128] = {0};
int n = recv(c,buff,127,0);
if ( n <= 0)// recv返回值n == 0 说明客户但关闭连接了
{
break;
}
printf("recv=%s\n",buff);
send(c,"ok",2,0);
}
close(c);//关闭连接 挥手
printf("client close\n");
}
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);//套接字 文件描述符
if ( sockfd == -1 )
{
exit(1);
}
struct sockaddr_in saddr,caddr;//套接字地址 ip port
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("192.168.84.248");
int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
if ( res == -1 )
{
printf("bind err\n");
exit(1);
}
res = listen(sockfd,5);
if ( res == -1 )
{
exit(1);
}
while( 1 )
{
int c = accept(sockfd,NULL,NULL);
if ( c < 0 )
{
continue;
}
printf("accept c=%d\n",c);
pid_t pid=fork();
if(pid<0){
printf("fork err\n");
close(c);
continue;
}
if(pid==0){
close(sockfd);
fun(c);
exit(0);
}
}
}
使用命令
netstat -natp
三次握手时:
四次挥手时:
UDP 数据报服务特点:发送端应用程序每执行一次写操作,UDP 模块就将其封装成一个 UDP 数据报发送。接收端必须及时针对每一个 UDP 数据报执行读操作,否则就会丢包。并且,如果用户没有指定足够的应用程序缓冲区来读取 UDP 数据,则 UDP 数据将被截断。
与TCP不同的是UDP接收到的数据报会直接发给接收端,不会存在缓存区。
(1)UDP是一个非连接的协议,传输数据之前,源端和终端不建立连接,当它想传送时,就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。
在发送端,UDP传送数据的速度仅受应用程序生成数据的速度、计算机的能力和传输带宽的限制;在接收端,UDP把每个消息段放在队列中,应用程序每次从队列中读一个消息段。
(2)由于传输数据不建立连接,因此不需要维护连接状态,包括收发状态等。一台服务机可同时向多个客户机传输相同的消息。
(3)UDP信息包的包头很短,只有8字节,相对于TCP的20字节包头信息,UDP的包头开销很小。
(4)吞吐量不受拥挤控制算法的调节,只受应用软件生成数据的速率、传输带宽、源端和终端主机性能的限制。
(5)UDP会尽最大努力去传输和接受数据且没有限制,但并不保证可靠的数据交付,主机也不需要维持复杂的链接状态表(里面有许多参数)。
(6)UDP是面向报文的。发送方的UDP对应用程序传过来的报文,在添加包头后就向下交付给IP层。既不拆分,也不合并,而只是保留这些报文的边界,因此,应用程序需要自己限制合适的报文大小,以免报文太大导致丢失率高。
UDP 数据读写:
recvfrom()读取 sockfd 上的数据,buff 和 len 参数分别指定读缓冲区的位置和大
小
src_addr 记录发送端的 socket 地址
addrlen 指定该地址的长度
sendto()往 socket 上写入数据,buff 和 len 参数分别指定写缓冲区的位置和数据长
度
dest_addr 指定接收数据端的 socket 地址
addrlen 指定该地址的长度
ssize_t recvfrom(int sockfd, void buff, size_t len, int flags,struct sockaddr src_addr, socklen_t *addrlen);
ssize_t sendto(int sockfd, void buff, size_t len, int flags,struct sockaddr dest_addr, socklen_t addrlen);
#include
#include
#include
#include
#include
#include
#include
int main()
{
int sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(sockfd==-1)
{
printf("socket err\n");
exit(1);
}
struct sockaddr_in saddr,caddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family=AF_INET;
saddr.sin_port=htons(6000);
saddr.sin_addr.s_addr=inet_addr("127.0.0.1");
int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
if(res==-1)
{
printf("bind err\n");
exit(0);
}
while(1)
{
int len=sizeof(caddr);
char buff[128]={0};
recvfrom(sockfd,buff,127,0,(struct sockaddr*)&caddr,&len);
printf("buff=%s\n",buff);
sendto(sockfd,"ok",2,0,(struct sockaddr*)&caddr,sizeof(caddr));
}
close(sockfd);
exit(0);
}
#include
#include
#include
#include
#include
#include
#include
int main()
{
int sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(sockfd==-1)
{
printf("socket err\n");
exit(1);
}
struct sockaddr_in saddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family=AF_INET;
saddr.sin_port=htons(6000);
saddr.sin_addr.s_addr=inet_addr("127.0.0.1");
while(1)
{
print("input:\n");
char buff[128]={0};
fgets(buff,128,stdin);
if(strncmp(buff,"end",3)==0)
{
break;
}
sendto(sockfd,buff,strlen(buff)-1,0,(struct sockaddr*)&saddr,sizeof(saddr));
memset(buff,0,128);
recvfrom(sockfd,buff,127,0,NULL,NULL);
printf("buff=%s\n",buff);
}
close(sockfd);
exit(0);
}
四、TCP和UDP区别
TCP安全、可靠、面向连接,但是传输速度慢。UDP不安全、不可靠、面向非连接,但是传输速度快。
TCP更适合与文件传输,如果出现错误,会重发。
UDP更适合于实时性,如视频通话,在网卡的情况下,卡住,网好后,能够将实时的画面更新,而不会把之前的再发一遍。