前言
1. 基础知识
协议就是我们在网络传输中的一组规则
网络有模型
OSI 7层模型:应用层,表示层,会话层,传输层,网络层,数据链路层,物理层
TCP/IP 4层模型:应用层(应用层,表示层,会话层),传输层,网络层,网络接口层(数据链路层,物理层)
TCP/IP模型就是我们经常使用的
应用层:http等,ftp,nfs,ssh,telnet
传输层:TCP等 UDP
网络层:IP等 ICMP,IGMP
链路层:以太网帧协议,ARP
数据在传输的过程中,先包装应用层,然后是传输层,网络层,链路层,才能传输在网络中,到达目的地,在打开这些包装
其中应用层是需要我们来干的,其他的都是系统干的
没有分装就不能在网络中传递
ARP数据格式
通过找到了目的IP地址,就可以找到以太网目的地址了
先出去找(请求)
找到了目的IP再返回以太网目的地址(应答)
意思就是ARP协议就是根据IP地址获取mac地址
以太网帧协议就是根据mac地址,完成数据的传递
每个网络设备都有一个mac地址
IP协议
首先讲一下TTL,为防止数据在网络中一直传递,因为有些时候可能设备之间的链接断了,这样就会一直传递
所以设置一个TTL,就是数据在网络中跳转的次数限制
IP版本:IPv4,IPv6
TTL:每经过一个路由节点,-1,当为0的时候,这个数据就会被抛弃
源IP:32位-----4字节 192.168.1.108 这个是十进制的IP地址,是字符串类型的 但是在网络中的IP是二进制的
目的IP:32位-----4字节
TCP就是和端口相关的
IP地址:在网络环境中,唯一标识一台主机
端口号:在网络上的一台主机上唯一标识一个进程
IP地址和端口号:可以在网络环境中唯一标识一个进程
UDP协议:
16位:源端口
16位:目的端口
TCP协议:
16位:源端口
16位:目的端口
32序号
32确认序号
6个标志位
16窗口大小
下面讲一下,cs模型和bs模型的区别
cs就是我们平时下的应用,比如哔哩哔哩
bs是浏览器访问的,比如浏览器上的哔哩哔哩
cs:优点:缓存大量数据(更好看,更漂亮,比如王者荣耀),协议选择灵活(可以不遵守一些协议,自己制定协议),速度快
缺点:不安全(下载万一有病毒那些呢),跨平台差,有些手机就不能下某些应用,开发工作量大,比如王者很大
bs:优点:安全,跨平台,有网址,不管什么浏览器都可以访问,开发工作量小
缺点:不能缓存大量数据,严格遵守http
2. 套接字
套接字就相当于插板和插座的关系
所以一定是成对出现的
一个源套接字(用户端),一个目的套接字(服务端)
都有发送端,接收端
网络字节序
在我们的计算机中,数据的存储是小端存储的,但是网络上是大端存储的,所以网络上的数据和我们计算机中的不一样,所以要转换一下
小端:高位存高地址,低位存低地址
htonl
htonl函数的主要功能是将主机字节顺序(小端)转换为网络字节顺序(大端)htonl函数常用于处理IP地址的字节顺序转换
我们知道的IP形式是192.168.1.110就是公网IP,这个是字符串形式的十进制,但是参数是整型,怎么办呢
我们可以用atoi,把字符串转换为int
192.168.1.110->string->atoi->int->htonl->网络字节序
其他
htons:本地->网络(port端口)
ntons:网络->本地(IP)
ntohs:网络->本地(port)
这些都是以前使用的函数,下面介绍一些新的
IP地址转换函数
inet_pton
专门给IP的,port就不用了
cpp
int inet_pton(int af, const char *src, void *dst);
af:指的是IP版本,AF_INET(我们常用),AF_INET6
src:IP地址(点分十进制,就是192.168.1.110这个形式)
dst:传出,转化后的网络字节序的IP地址,放在dst地址所指向的内容,应该是一个整型
返回值:成功:1
异常:0,说明双人床、指向的不是一个有效的IP地址
失败:-1
inet_ntop
cpp
const char *inet_ntop(int af, const void *src,char *dst, socklen_t size);
网络字节序-》本地字节序
af:AF_INET,AF_INET6
src:网络字节序IP地址
dst:本地字节序(string IP)
size:dst的大小
返回值:成功:返回dst
失败:NULL
sockaddr地址结构
下面来讲一下sochet创建流程
这些全部都是函数
比如这个函数
有一个参数类型是sockaddr
值得注意的就是sockaddr我们已经不用了,我们现在用的都是sockaddr_in
这个就是它的地址结构
所以我们在使用bind的时候,先要创建sockaddr_in,在强制类型转换区传参
cpp
struct sochaddr_in addr;
bind(fd,(Struct sockaddr*)&addr,size);
除了bind是这样的,还有connect等
cpp
struct sockaddr_in addr;
//初始化addr,依次填充成员变量就可以了
addr.sin_family=AF_INET/AF_INET6;
addr.sin_port=htons(9527);//端口号//本地端口9527转换为网络上的端口
int dst; addr.sin_addr.s_addr=inet_pton(AF_INET,"192.157.22.45",(void*)&dst);//要的就是网络上的IP,所以这样干
//结构体套结构体也一样的初始化
bind(fd,(struct sockaddr*)&addr,size);
或者这样干
cpp
struct sockaddr_in addr;
//初始化addr,依次填充成员变量就可以了
addr.sin_family=AF_INET/AF_INET6;
addr.sin_port=htons(9527);//端口号//本地端口9527转换为网络上的端口
int dst;
inet_pton(AF_INET,"192.157.22.45",(void*)&dst);
addr.sin_addr.s_addr=dest;//因为网络上的IP就是整型,32位的
bind(fd,(struct sockaddr*)&addr,size);
再或者这样
cpp
struct sockaddr_in addr;
addr.sin_family=AF_INET/AF_INET6;
addr.sin_port=htons(9527);
addr.sin_addr.s_addr=htonl(INADDR_ANY);//这个就是在系统中取出任意一个IP地址,,因为我们一般只有一个IP,所以没有什么区别其实
bind(fd,(struct sockaddr*)&addr,size);
socket模型创建流程
首先在服务器端,socket会创建一个socket
bind会绑定客户端的IP和port,
listen会设置监听上限,什么意思呢,这个意思就是同时能和几个客户网络联系,
accept会阻塞监听用户端连接,什么意思呢,意思就是,服务端要等,等客户端connect的时候,然后建立连接,并创建一个一模一样的socket,去连接客户端,自己这个socket就担当监听的功能,所以网络通信的时候,一般有三个socket
socket
cpp
#include<sys/socket.h>
int socket(int domain, int type, int protocol);
domain:AF_INET,AF_INET6,AF_UNIX
type:指定socket的类型,有三种
SOCK_STREAM:这是TCP中以流的方式传输类型,用于可靠的、面向连接的通信。一般用这个
SOCK_DGRAM:这是UDP的数据包传输类型,用于不可靠的、无连接的通信。
SOCK_RAW:这种类型的socket用于接收原始的数据包,允许直接访问底层网络协议的数据。
protocol:选定协议类型,为0,就是选的默认的
返回值:成功:新套接字的所对应的文件描述符,也就是这个socket的句柄,就是这个socket,可以控制这个socket
失败:-1 errno
cpp
fd=socket(AF_INET,SOCK_STREAM,0);
bind
cpp
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
给这个socket绑定对应的客户端的IP和port
sockfd:sock函数返回值
cpp
struct sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_port=htons(8888);
addr.sin_addr.s_addr=htonl(INADDR_ANY);
addr:(struct sockaddr*)&addr
addrlen:sizeof(addr):地址结构的大小
返回值:成功:0
失败:-1 errno
listen
cpp
int listen(int sockfd, int backlog);
sockfd:sock函数返回值
backlog:与服务器建立连接的上限数,但是就算你设置了次数,系统还是默认的最大128
返回值:成功---0
失败:-1 errno
accept
cpp
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
阻塞等待与客户端建立连接,成功的话,返回一个与客户端建立连接成功的socket文件描述符,这个socket文件描述符是自己服务端的新的
sockfd:sock函数返回值
addr:传出参数,成功与服务器建立连接的那个客户端的地址结构(IP+port)
addrlen:入:addr的大小,出:客户端addr的实际大小
cpp
socklen_t clit_addr_len=sizeof(addr);
addrlen:&clit_addr_len
返回值:能与服务器进行数据通信的socket对应的文件描述
失败:-1,errno
connect
cpp
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
这个的作用就是与服务端建立连接
sockfd:客户端socket函数返回值
addr:传入型参数,意思是addr对应的要初始化好,而且这个是服务器的addr,服务器的地址结构
addrlen:服务器地址结构的大小
返回值,成功 0
失败-1 errno(意思是有错误码)
客户端不用bind,因为它是隐式绑定的,自己隐式有一个端口,绑定自己机器的IP
read
cpp
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
fd就是socket的返回值,谁来读,就传谁的fd
write
cpp
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
流程
server:服务端
- socket() 创建服务端的socket
- bind 绑定服务器的地址结构
- listen 设置监听上限
- accept 阻塞监听客户端连接
- read 读取获取客户端数据
- 小写转大写
- write
- close
client:客户端 - socket()
- connect 与服务器建立连接
- write
- read
- 显示
- close
3. socket模型创建
如何求我们计算机的IP呢,
cpp
ip addr show
就可以了
其中那个127.0.0.1就是我们的IP
sz指令可以把我们linux中的文件放在windows中
然后直接拖动windows中的文件,就可以把windows中的文件放在linux中了
cpp
#include<sys/types.h>
#include<sys/socket.h>
#include<stdio.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<stdlib.h>
#include<ctype.h>
#define PORT 9527//给我们的服务端设置一个端口
void sys_err(const char*str)
{
perror(str);
exit(1);
}
int main()
{
int sfd=socket(AF_INET,SOCK_STREAM,0);//创建套接字
//bind给套接字绑定端口和IP
// int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
//首先得创建一个addr,这个里面存的就是服务端的端口和IP,因为const是传入型的,你要初始化好,addrlen就是addr的大小了
struct sockaddr_in saddr;
saddr.sin_family=AF_INET;
saddr.sin_port=htons(PORT);//因为传的是网络版的
//saddr.sin_addr.s_addr=传入的是网路版的IP
saddr.sin_addr.s_addr=htonl(INADDR_ANY);//INADDR_ANY是默认的我们这个计算机的IP
bind(sfd,(struct sockaddr*)&saddr,sizeof(saddr));//第二个参数强转一下就可以了
listen(sfd,128);//设置最大访问数
// int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
//阻塞等待与客户端的连接
//addr是输出型参数,意思就是我们传进去的地址,指向内容不用初始化,返回的就是客户端的socket的地址结构,返回的是
//我们在服务端新创建的一个socket的句柄
//第三个参数的意思就是我们传进去时,是我们服务端的地址结构的大小,传出来的时候就是客户端socket地址结构的大小了
struct sockaddr_in laddr;
socklen_t addrlen=sizeof(saddr);
int sfd2=accept(sfd,(struct sockaddr*)&laddr,&addrlen);
//ssize_t read(int fd, void *buf, size_t count);
//第一个参数就是说,我们要在哪里读,在服务端,就输入服务端的fd,就是读到buf里面,count是buf的大小
char buf[BUFSIZ]={0};//这个是系统自带的#define的,是个很大的数字,,左括号和d就可以查看
while(1)
{
read(sfd2,buf,BUFSIZ);
//这下就读到buf中了
//接下来把buf中的所有字符都转成大写的
for(int i=0;i<BUFSIZ;i++)
{
buf[i]=toupper(buf[i]);//这个就是把小写转大写的函数
}
write(STDOUT_FILENO,buf,BUFSIZ);//把buf返回给屏幕,就是打印的意思
//ssize_t read(int fd, void *buf, size_t count);
//fd就是我们现在服务端的socket
write(sfd2,buf,BUFSIZ);//返回给客户端
}
close(sfd);//关闭的是socket
close(sfd2);
return 0;
}
cpp
#include<sys/types.h>
#include<sys/socket.h>
#include<stdio.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<stdlib.h>
#include<ctype.h>
#define SERVE_PORT 9527//给我们的服务端设置一个端口
void sys_err(const char*str)
{
perror(str);
exit(1);
}
int main()
{
int cfd=socket(AF_INET,SOCK_STREAM,0);//创建套接字
//客户端的socket不用bind,意思就是不用完成端口,和IP的输入到地址结构中,因为
// 客户端会默认bind的,默认给自己这个进程设置一个端口,IP就是对应机器的IP了
//int connect(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
//与服务器端绑定
//addr是服务器的地址结构,addrlen就是其对应的大小
struct sockaddr_in saddr;
saddr.sin_family=AF_INET;
saddr.sin_port=htons(SERVE_PORT);
//saddr.sin_addr.s_addr=htonl(INADDR_ANY);//因为我们用的一个机器,所以可以这样,但是我们知道服务器的IP了,和端口了,不应该这样
//写,万一不在一台机器上测试呢
// int inet_pton(int af, const char *src, void *dst);///这个是把我们的string转换成网络字节序
inet_pton(AF_INET, "127.0.0.1", &saddr.sin_addr.s_addr);
connect(cfd, (struct sockaddr*)&saddr, sizeof(saddr));
//ssize_t read(int fd, void *buf, size_t count);
// ssize_t write(int fd, const void *buf, size_t count);
char arr[BUFSIZ] = { 0 };
int count = 5;
while (--count)
{
write(cfd, "hahaha", 6);//向服务器端写入,写入的个数为6
read(cfd, arr, BUFSIZ);//从服务器端读取
write(STDOUT_FILENO, arr, BUFSIZ);//向屏幕写入
}
close(cfd);
return 0;
}
这个就是客户端的逻辑
接下来我们在服务端试一下打印客户端的端口和IP地址