网络编程--socket编程

套接字

概念

在这里插入图片描述
Socket本身有插座的意思,但他是进程之间网络通信的一种特殊文件,本质是缓冲区形成的伪文件,
所以,网络进程之间的数据传递,主要依靠套接字文件

通信原理

在这里插入图片描述
Socket有插头插座的意思,所以,如果想要实现网络进程之间的通信,套接字必须成对出现
在这里插入图片描述
由于套接字是一个特殊的缓存区形成的文件,所以可以使用文件描述符引用套接字,并可以借助文件描述符进行数据的读写操作,实现网络进程之间的数据传输
在这里插入图片描述

总结

在这里插入图片描述

预备知识

网络字节序

简介

在这里插入图片描述
问题产生:计算机本地使用的是小端法进行二进制的存储,即高位高地址,低位低地址。
但是网络流中是使用的大端法,所以要想实现通信的正常进行,就要进行转换

字节转换函数

在这里插入图片描述
htonl 将其拆分进行记忆
例如 htonl 拆分成 h to n l
h是本地,to是到,n是网络,l是long型,表示32位即4字节
所以是本地转向网络,且是long型数据,所以针对的是IP
ntohs 拆分成 n to h s
表示从网络到本地,且是short型,16位2字节,等效于int,所以针对的是端口号(port)
在这里插入图片描述
以从本地到网络为例:如果主机是小端字序,那么函数就发挥了应有的作用,转为大端字序然后返回,如果主机本来就是大端字节序,那么这些函数不做转换,将参数原封不动进行返回,总之函数结果是大端字节序
从网络到本地恰恰相反,函数的结果是小端字节序

IP地址转换函数

为什么单独列出

因为使用上面的字节转换函数,都是参数为整型时才可以使用(long short int都是整型)
而接下来的IP地址转换函数,是直接进行string与整型的转换

函数原型

在这里插入图片描述
在这里插入图片描述
以inet_pton函数为例,因为下划线后面是p to n,所以是本地字节序转网络字节序
函数参数第一个是IP版本,分为IPv4与IPv6,对于这两个选项有两个宏,分别是AF_INEF、AF_INET6
第二个参数传入本地IP地址(形式是点分十进制)
第三个参数是dst指针,利用该参数进行数据的返回,一个指针存储转换完成后的网络字节序类型的IP地址

而函数自己的返回值是int,有三个数1、0、-1,具体含义在上图列出,
所以说 第一个函数有两个返回值,一个是函数自己的返回值,表示状态(是否成功)
另一个是通过指针参数返回,返回具体的网络字节序

第二个函数inet_ntop函数,表示从网络字节序到本地字节序
第一个参数是版本号,第二个参数是网络字节序类型的IP地址,第三个参数是转换完成后的本地字节序(string类型)类型的IP地址,最后一个参数是dst的大小

小tips:在Linux命令行中输入man 函数名
会显示该函数的帮助文档
在这里插入图片描述
在这里插入图片描述

sockaddr结构体

在这里插入图片描述
具体关于sockaddr的解释,在Linux命令行中输入man 7 ip即可查看

sockaddr结构体,是一组数据的集合,现在被优化成了两个版本,分别是sockaddr_in 以及 sockaddr_in6,分别表示IPv4以及IPv6,如下图所示:
在这里插入图片描述
在这里插入图片描述
在之后的许多关键函数中,函数参数都是sockaddr,(如上图所示)但是我们现在都是使用sockaddr_in或者sockaddr_in6,如何解决这个问题呢:
在这里插入图片描述
我们在定义结构体时,就用现在更高级的sockaddr_in类型来定义,例如我们定义的结构体变量是addr
然后我们在向一些原型是sockaddr的函数传参时,将sockaddr_in类型变量的地址进行一个强转,转为 struct sockaddr * 类型,如上图

然而对于sockaddr_in结构体,有如下所示成员
在这里插入图片描述
我们在定义结构体之后,同时要对其成员进行初始化,
第一个成员是sin_family,他是IP版本,赋值为两个版本的宏
第二个成员是sin_port,根据上图的注释可以看出,这个端口号要求是网络字节序的端口号,我们可以使用前面学习的字节转换函数htons(传入本地端口号(整型))
第三个成员是一个结构体sin_addr,他里面之后一个成员uint32_t,根据注释,可知他是一个网络字节序的IP地址,对于这一步的初始化,最常用的下图中的【*】语句,即使用一个库宏INADDR_ANY,他表示系统任意一个有效的IP地址,二进制整型,这里就是得到本机的IP地址,所以可以直接传入htonl函数,得到网络字节序的IP地址,赋值给第三个成员
在这里插入图片描述

套接字函数

简介+目标

我们可以通过套接字以及套接字函数完成一个socket模型(C/S)的建立,如下图:(实现一个小写转大写的操作)
在这里插入图片描述
在这里插入图片描述
注意,对于一对套接字的通信,还要有第三个套接字,在上图右上角,参与监听功能

服务端

在这里插入图片描述
首先服务端创建一个套接字socket,该套接字的文件描述符是fd,称为句柄,他是套接字文件的唯一入口,用它来读写套接字文件

之后用bind函数对该套接字设置IP+port

之后listen函数用来设置监听上限,在下面会有详细介绍

然后accept函数用来阻塞监听客户端的连接,当到这一步时,就用到了刚刚创建的那个套接字文件,以套接字文件为参数传入该函数,该函数会返回一个新的套接字文件,就是真正用来进行网络通信的服务端套接字文件

补充:;listen函数用来设置监听上限,即设置一个服务端同时最多能与几个客户端进行网络通信,如果参数设置为20,那么表示服务端最多能同时与20个客户端进行通信
在这里插入图片描述

客户端

在这里插入图片描述
客户端也需要创建一个套接字文件,且自始至终一直是一个套接字文件,首先创建出套接字文件,之后调用connect()函数,绑定IP+port(这个地址结构是服务端的地址结构),connect绑定服务端的地址结构,从而建立连接,与此同时,服务端的accpet会传出与之绑定的客户端的地址结构

该函数调用之后,就形成了上图中双向箭头的状态

socket函数

在这里插入图片描述
功能:创建一个规定了IP版本以及传输协议的套接字文件

首先要包含头文件<sys/socket.h>
第一个参数:指定IP版本,同样是IPv4,与IPv6两个宏
第二个参数:指定通信协议,上面两个宏分别代表流式协议和报式协议
第三个参数:指定第二个参数两个协议的代表协议,一般是0,表示如果第二个参数是流式协议,则就是TCP协议,第二个参数是报式协议,则就是UDP协议

返回值:成功,则返回所创建的套接字文件的文件描述符
失败,则返回-1,同时设置errno,如下图描述(errno是一个字符串变量,使用时包含errno.h即可,他会在函数发生异常时,捕捉到异常,并存入错误信息到宏errno中,可以使用perror函数,输出一段自定义语句之后,该函数会在自定义语句之后拼接上系统捕捉的错误信息,详情见“系统编程–文件IO”)
在这里插入图片描述

bind函数

在这里插入图片描述
在这里插入图片描述
tip:该头文件与bind配合使用

功能:给socket文件绑定一个地址结构

头文件是<sys/socket.h>
<arpa/inet.h>
第一个参数,是socket的文件描述符,由socket函数返回
第二个参数,是要绑定的地址结构(IP+port)
传入一个sockaddr_in 变量,首先要进行初始化,如上图
第一个参数是IP版本
第二个参数是服务器端口号的网络字节序
第三个参数是服务器IP地址的网络字节序
最后将addr的地址强转后赋值
第三个参数,是addr的大小

返回值,成功的话返回,失败返回-1

listen函数

在这里插入图片描述
功能:设置服务器的监听数量上限
第一个参数,传入socket套接字的文件描述符(socket函数的返回值)
第二个参数:设置能同时与服务器建立连接的上限数(最大设置为128,但是其实在底层,系统会做优化,不管你传入0到128的任何一个数,都会被系统优化为128)

accept函数

在这里插入图片描述
功能:阻塞等待客户端建立连接,成功连接的话,则返回一个与客户端成功连接的socket文件(即该函数会等待客户端的connect函数来申请连接)

第一个参数:之前第一个创建的socket函数返回值
第二个参数:这是一个传出参数,也就是用于返回一个值,(调用时,传入一个未初始化的相应类型的变量即可)
返回与服务器成功建立连接的客户端的地址结构
第三个参数:传入传出参数,
传入的是addr的sizeof,但是要定义为socklen_t 类型(实际上还是用来接收sizeof,传的时候取地址),传入的是指针,所以可能这里指的就是addrlen指针的大小(4字节)
传出的是实际的客户端的addr的大小

返回值:成功的话,返回与客户端建立连接的socket文件描述符
失败的话,返回-1

connect函数

在这里插入图片描述
功能:使用现有的已经创建好了的socket文件与服务器建立连接

第一个参数:客户端创建的socket文件的文件描述符,也就是socket函数的返回值
第二个参数:传入参数。要连接的服务器的地址结构(即sockaddr结构体)
第三个参数:服务器的地址结构的大小

返回值: 成功返回0
失败返回-1,errno

注意到在客户端创建了socket套接字文件之后,并没有像服务器那样使用bind函数将客户机的地址结构与socket文件进行绑定,如果不使用bind进行客户端地址结构与客户端socket文件的绑定的话,系统会采用“隐式绑定”

C/S模型的TCP通信

C/S模型的TCP通信流程分析

在这里插入图片描述
首先对于服务器端:
创建socket
绑定服务器地址结构
设置监听上限
阻塞监听客户端连接
通过文件描述符,读socket获取客户端数据
(因为套接字文件本质上就是缓冲区文件,相当于Windows里的控制台,可以看成服务器的socket套接字文件是一个控制台文档文件,而客户端的socket由于与服务器建立了连接,他的socket套接字文件也是一个控制台文档文件,并且与服务器端的文档文件实时同步
这里客户端先在自己的socket套接字文件里写入了原始数据,所以更新到服务器的socket文件(看成控制台)里就会出现原始数据,通过该服务器socket的文件描述符来读取socket套接字文件,系统就会去读取到控制台里面的原始数据)
做相应的操作
通过文件描述符,将结果写入socket文件
(这里通过该socket的文件描述符将结果写入服务器的socket套接字文件,系统会写入到文档文件,并且实时更新到客户端的socket文件的文档文件)
close
对于客户端:
创建socket
与服务器建立连接
写原始数据到socket(先向自己的socket套接字文件里写入原始数据,相当于向客户端的控制台文件写入原始数据,并且由于建立了连接,会实时更新到服务器的socket套接字文件)
读服务器转换完之后的结果(因为与服务器socket文件建立了连接,所以当服务器将处理后的数据写入自己的socket文件之后,会实时更新到客户端的socket文件里,客户端可以利用文件描述符进行读取)
显示读取结果
close

!!! 上述所说的看成控制台,只是他的性质与控制台一样都是数据缓冲区,便于理解,并不会真的去显示,如果想要显示,要写相应的显示语句

server实现

tips

在这里插入图片描述
想要查看一个宏的值,可以将光标放在哪里之后,左中括号+d进行查看具体的值

代码实现

在这里插入图片描述
在这里插入图片描述
需要注意的点:
1、while循环的作用,一直在监听客户端的数据,一旦有数据,就会进行read
2、while循环,条件为1,是死循环,这里是为了测试用,真正开发时,要设置循环跳出条件
3、read函数,传入文件描述符缓冲区,字符数组,以及大小,表示将cfd缓冲区中的数据读入到buf字符数组中,返回值为读到的字节数(可以读入空格和回车,但是不会读入最后的结束标志),而字符数组可以使用一个宏BUFSIZ,他的大小是4096/8192
4、write函数,传入文件描述符缓冲区,字符数组,第二个参数中有效数据的字节数,表示将buf的内容写入到cfd缓冲区
5、记得包含头文件,<ctype.h><sys_socket.h><arpa/inet.h>
其中,ctype.h,是toupper函数的头文件,sys_socket.h,是一些套接字函数的头文件,最后一个,是与bind函数配合的头文件,一定要加上
6、在while循环中,第一行是read读入数据,第二行是write(STDOUT_FILENO, buf,ret)意为将buf中的内容写到显示屏进行显示(这里的显示屏是指服务端的显示屏)
7、可以看到这里进行了多次write,说明,write,是将buf中的数据拷贝之后,再进行的发送,其函数调用并不会更改buf中的数据

测试(使用nc命令)

在这里插入图片描述
由于我们目前没有写客户端的代码,所以,可以使用一个命令来模拟客户端,nc命令
新开一个终端,输入nc ip 端口号,由于是本机测试,所以ip就是127.0.0.1
这样,我们就连上服务器了,可以进行测试了

补充

在这里插入图片描述
我们可以拿到客户端的地址结构(网络字节序),有了地址结构,就可以拿到他的IP和port了(注意要转为本地字节序,适配linux系统)
之所以inet_ntop的第二个参数中,是用clit_addr去点调用,而不是箭头调用,是因为该参数前面的取地址是针对的后面整个变量名而不是单一个clit_addr,且虽然上面accept传入的是地址,但是accept函数是在该地址上的变量的值做了改变,当accept函数执行完之后,clit_addr的内容就变了,变为了客户端的地址结构,所以可以直接拿着该变量进行相关变量的访问

client实现

代码实现

在这里插入图片描述
在这里插入图片描述
纠错:
serv_addr.port在赋值时需要使用htons,将端口
注意点:
1、connect函数中,第二个参数需要一个地址结构,该地址结构存储的是服务端的地址结构,所以,我们创建出sockaddr_in类型的地址结构,然后对其赋值,要注意赋成服务端的地址结构:
第一二个成员好赋值,主要是第三个成员,我们要获取到服务器的IP地址,并且转为本地字节序,所以可以直接使用inet_pton函数,第二个参数写入本地IP,第三个参数表示的是,将处理完之后的结果给到形参地址,所以可以直接用地址结构的第三个成员的地址当做形参,就完成了对其的初始化
2、在循环中,先是向cfd中写入数据,表示客户端输入的原始数据,之后,从cfd再读取服务端所返回的数据,读到buf字符数组中,之后,还没完,还要将字符数组的内容写到屏幕上,STDOUT_FILENO
且为了不会一次性全部输出,可以加一个睡眠,sleep(1),表示每次睡眠一秒

函数处理错误的封装思路

思想

因为我们在写程序时,总会用一个变量接住函数的返回值,对返回值进行判断,看是否返回的是发生异常时的返回值(例如如果返回的是异常的返回值,则打印“程序异常”)
这样可以增强程序的健壮性,但是同时也会多出许多与核心逻辑无关的代码,但又无法删去(为了程序的健壮性)

所以我们可以对原来官方封装的API进行改造,将异常处理包含在函数体内(即 将打印“程序异常”这一动作放入函数体内),然后改一个名字,以后就用自己改造好的函数

示例

原本:
在这里插入图片描述
对API进行改造:
在这里插入图片描述
在这里插入图片描述

积累

1、规定读取的字节数的read函数
在这里插入图片描述
2、规定写入的字节数的write函数
在这里插入图片描述
3、规定一次一行读取的read函数
这是一个为readline服务的内部函数,在readline中要调用my_read
在这里插入图片描述
在这里插入图片描述

总结

在这里插入图片描述
注意写出来wrap.c之后,要将其与调用他的文件一起编译,即进行联合编译,生成可执行文件
同时,封装后的函数名,将其首字母改成大写,这样的话仍然可以通过函数名跳转到manpage帮助手册

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值