TCP socket api详解

文章目录

创建套接字socket + sockaddr_in结构体 + bind

之后就和UDP不一样了。

因为TCP是一个面向连接的服务器,在正式通信之前,他一定得随时随地等别人先和我连接上再说

也就是通信前得先建立连接,服务期就得一直想办法等别人来连。

第三个基本工作

listen

将套接字设置为监听状态,他才能随时听到有客户来连我了,然后我就可以帮我们把连接获取上来,然后基于连接再通信。

sockfd网络文件描述符

backlog 全连接队列 一般10不要设置的太大,后面讲tcp协议再谈。

netstat -nltp

n 显示数字

l listen状态

t tcp

p 进程

这里虽然tcpserver 绑定的是0.0.0.0 但是你客户端访问的时候是拿着公网ip去访问的,所以能找到。

accept

把服务器设置为listen状态完全不够,所以还要建立链接

返回值

成功,返回一个整数文件描述符,从你设置为监听状态的sockfd里面获取

所以tcp搞出多个文件描述符

accept返回又一个套接字,那我到底用哪个通信?

好再来鱼庄

一个门口的拉客服务员张三

张三不服务具体客人,他只是拉客

真正提供服务的是店内的李四

我们曾经的监听套接字,他的核心工作就是从底层把新连接获取上来

而提供IO数据通信服务的,是accept返回的fd。

监听套接字一般只有一个

accept返回值fd可以有多个

我们把经历过bind + listen的这个套接字 直接叫做listensock

如果listensock获取连接失败了,错误等级顶多也就是waning,然后直接continue继续获取连接

也就是张三拉客失败,你不去吃鱼,那 他就转头去找另一个小哥哥小姐姐问来不来吃鱼呀?

从上面到下面,我们讲的所有接口都是阻塞的。

也就是张三拉客路边根本没人,所以张三只能在那等着。

当然未来可以设置为非阻塞。

简单客户端工具 telnet 指定服务连接

127.0.0.1 本地换回 通过网络层从下往上 返回给本主机

telnet默认使用tcp协议

回车 ctrl+] quit

无论UDP and tcp ,云服务器的公网IP无法绑定进套接字

然后我就从accept返回的fd 通信呗

我还要获取从accpet返回给我哪一个客户端连接我的ip和端口信息

char ipstr[32]

4字节 转 点分十进制ip地址 需要多少空间?

点分十进制 四个区域,每个区域3个数字,就是12再加上三个点15,32是保守点

问题

不仅仅是IP和端口会发送到网络里,你做了大小端转化处理

那我们发过去的字符串消息 难道不做大小端转化吗?

也就是recvfrom sendto read write 这样的接口难道不用大小端转化吗

recvfrom sendto 读上来我也没见你转呀,最后通信也没问题

正常的通信内容,你所使用的接口会自动转化,只不过端口和ip很特殊,是要给OS的。

正常通信时不用操心大小端问题。

管道的读取 支持字节流能用read write

文件读取 也是支持字节流的 也直接read write

tcp 也是面向字节流的

所以读网络就像读取文件一样。

目前只处理正常的情况


tcp客户端

tcp客户端需不要bind?

一定要,但是不需要显示编码bind

为什么?

socket通信必须用ip+port 对于客户端来说保证唯一性就可以了,端口是几不重要。

由OS自动随机选择。

tcp这里什么时候自动绑定呢?

那我就打开网络文件描述符然后关闭它,在这之间我们tcp客户端需要干什么呢?

记住了,tcp是面向连接的,所以注定了客户端你得链接服务器,连接成功才能进行通信。客户端既然都不bind了,也就根本不存在listen,客户端也就不会让别人来连他,所以也就不会accept

客户端需要连别人,能向服务器发起连接的接口

connect

后两个参数和sendto是一毛一样的,意思是连接你是想向谁发起连接呢?

客户端发起connect的时候,进行自动随机bind

给谁发起连接,那就用命令行参数,你用户传我要连接哪个服务器ip+port

然后就是从文件描述符进行IO通信了

关于getline

会清空while循环外的string msg

异常处理

读写通信连接建立好,任何一方都可能出问题,服务器可能挂掉,客户端可能退出。

尤其对于服务器而言,如果客户端直接关闭自己连接怎么办?

客户端退了,服务端读取时会read返回0

返回0就跳出循环,service函数结束,关闭这个链接的描述符

read 返回值小于0 说明读取直接出错了 我们也让他break

而且获取新连接这件事是再死循环里面的,你关闭一个链接还能继续获取下一链接

如果服务器向已经关闭的套接字文件描述符写入时怎么办呢?

打游戏时,突然卡主不动了,当前你已掉线正在重连

这就是你的tcp客户端重新connnect

我们模拟一下


把早连上的客户端ctrl + c调之后下面的服务期就可以回显消息了。

为什么当前上面客户端可以发消息,下面的也不是发不了,只有上面退了下面才回显。

version 1 单进程版

这是因为目前为止我们是单进程版的服务,单进程版一旦进入service函数中只有给这个客户端服务完了执行流才会回到最开始,重新获取连接,重新进入服务函数提供服务。

你见过哪个服务器只能有一个客户服务,另一个客户你得等一等

这种服务器我们根本用不了。

就像餐厅里只有一张桌子。

所以今天

version 2 多进程版

每到一个新连接我们既想给新到的客户提供服务,提供服务期间还想让服务器继续处理其他对应的新连接,连接不断增多我也不怕。

fork创建子进程

子进程中执行服务函数,执行完了把fd关了,子进程也就退了

父进程呢就等待子进程,不等子进程会僵尸

这里有非常多的细节

文件描述符处理工作

  1. 文件描述符sockfd当前是不是父进程打开的文件描述符,会不会被子进程继承下去呢? 会的,父进程打开sockfd子进程能看到,子进程会和父进程虽然有两个不同的文件描述符表,子进程拷贝父进程Fd表,而子进程能看到父进程的sockfd,他连listensock也能看到,所以子进程不需要listensock直接关掉


    2. 先不考虑waitpid等待,因为这本身就不对 
    父进程他获取了新的套接字,他就回过头继续获取新连接了,他会重新获取文件描述符 ,可是你刚刚打开的sockfd已经被子进程继承下去了,这个sockfd指向打开的网络文件,那父进程还需要关心这个sockfd吗?
    相当于子进程和父进程看到的文件和Fd表都是一样的,只不过大家有不同的文件描述符表,此时子进程不关心listensock,他就关掉了。
    答:
    对父进程来讲,已经获取的新连接sockfd已经被子进程继承下去了,他就不需要了。
    父进程就把Sockfd关掉了。
    如果他不关,最后OS就可能有很多文件没有被关闭,最后就导致一些问题。
    .这和之前管道问题也是类似的。

    父进程每创建一个子进程就把sockfd交给子进程,由子进程全全负责给这个套接字提供服务。
    对于父进程必须要关掉曾经打开的fd,因为已经被子进程拿到了,不关导致父进程可用的Fd越用越少。

父进程把这个文件sockfd关闭了不会出问题吗,这个文件不会关闭吗?

父进程把他关了,这个文件不会被释放,因为文件struct file中包含引用计数,子进程还指向他呢。

我发现waitpid 为0 他是阻塞等待,子进程确实跑去执行服务了,可是父进程不是还在这里等待 吗,等你子进程执行完了退出了,我再继续获取连接accept。

这不和刚才单进程一样吗。

我想让父进程回到accept继续获取新连接,子进程继续服务,两个并发跑起来。

阻塞等待断然不能满足要求。

那我们把等待设置为非阻塞轮询,那也有点扯,非阻塞也可以,但不好

包括子进程退出会给父进程发信号,我们把等待工作放到信号处理中,这个也不想写。

所以两种做法。

  1. 子进程里面又继续fork创建了孙子进程,if(fork() >0) exit(0) 也就是子进程自己直接就退了,真正提供服务的是孙子进程
    因为子进程一启动创建立马退出,所以waitpid立马返回
    孙子进程提供服务的时候,waitpid已经回收了子进程的僵尸,然后waitpid返回直接继续accept
    有人就说,我们要不要等孙子进程呢?
    不用,你只要管好你儿子就行了。你儿子已经退出并且终止了,那你waitpid直接返回。
    所以孙子进程对他来讲,他的父进程直接就挂掉了,所以孙子进程会被系统领养,系统领养之后跟你有什么关系呢?
    孙子进程一旦把任务执行完了,这个孙子进程的资源就会被系统自动回收。
    在这份代码中,waitpid也要做,不然就会出现很多僵尸。

现象

我们每个人拿到的文件描述符都是4,这也很正常,因为3是listensock,获取一个新连接,就是4

你又关了然后继续获取还是4

我们能看到出了主进程其余都的子进程pid都是1,说明是孤儿进程。也就是被OS领养

  1. 其实我们可以不用等的,我们可以用signal将SIGCHLD的处理动作
    置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不 会产生僵尸进程,也不会通知父进程。

客户来了新连接,我才忙着创建子进程呢这是其一

第二,来了连接,子进程会变得越来越多,我们其实比较难对他进行处理的。

我们也可以提前创建一批进程,但是进程池我们不谈,创建进程成本非常高,创建进程,创建地址空间,pcb,维护页表,初始化各种数据,还要做一大堆各种工作,创建进程成本很高,所以我们不愿意这样做。

这样多进程版的服务器也就满足小型应用。我们一核2G云服务顶天跑20个顶天了。所以我们不可能用这样的代码。我们不想使用这种版本。

version 3 -- 多线程版本

创建线程,那我依然要等待新线程,还是在这里卡主,那accept也没有并发运行了。

此时新线程直接线程分离。因为主线程一直要获取新连接,创建出线程就不管了,让新线程跑去执行任务处理

第二个问题,

进程打开的所有文件描述符表,其他新创建出的线程能看见我曾经打开的listenscok,主线程获取到文件描述符新线程最终能不能通过文件描述符直接找到对应的文件呢?换句话说在线程这里要不要关闭不需要的文件描述符?

在线程当中,如果线程所在进程打开了一个文件,只要这个线程拿到对应的文件描述符,该线程直接可以访问这个文件,另外多线程这里完全不需要让多线程在关闭多余的文件描述符,因为他没有多余的。

答案是多线程这里不能关,一关就出大事了,线程大部分资源共享,比如文件描述符表。

为了让新线程提供服务,你要传文件描述符,你还需要客户端ip+port字段

那就打个结构体吧!

未来新线程负责套接字的整个生命周期

那Routine这样写能编过吗?

当前Routine必须是static的,因为类内函数自带的this指针与线程pthread_create的第三个形参类型不匹配

静态方法无法使用类内非static成员方法,也没有办法访问类内成员,因为他没有this指针。

那怎么办?

其实service函数和类内成员一点关系没有,你直接放到类外一样跑。

今天不想这么干

所以我们要把this指针传给新线程。

用this指针就可以访问类内非静态方法 了。

他比多进程好,创建的资源少。

可是每来一个客户都要创建新线程,那不会出问题吗?

客户不退,线程就越来越多,一般服务不是死循环,所以系统中不会维护太多线程,但是万一峰值情况,线程就会非常多,所以多线程版本也不适合应用于较大的应用。

来一个连接才给我创建线程,这也要花空间时间,我们这种情况登上就不退了,服务期就想办法提供一次服务。

所以

  1. 我们想预先创建好所有线程
  2. 我们不想提供长服务,否则提供短服务。
  3. 我们来了200个客户,每个客户都需要文件描述符,难道一定要有200个线程吗? 能不能限定线程个数的上限。
    每个用户访问,把访问变成对应任务,把任务交给后端。

version 4 ---- 线程池版本


先构建任务,什么任务呢?

你得需要IO通信吧,那就需要文件描述符,连接你的客户端的ip和端口, 主线程把这个任务push给线程池

push完之后呢,你就别管了,这个任务后端线程池自动处理,你的 主线程只需要回到最开始,继续accept.

别忘记服务器开始时,启动这个线程池,不要套进循环了

接下来的任务就是把通信service改成短时通信一次性通信的task类封装好

service本来就是可以放在类外的函数,直接放到task类里面的run方法里

读数据我只想给你返回一次,当然也可以构建任务再设置一些回调方法,让处理任务按照我们预期处理

今天就不倒来倒去的,今天继续是个简单echo服务器,但不想是个长服务,因为长服务在线程池里跑的话是不合理的,因为线程池里线程个数是确定的。

所以改成你只能访问一次,你给我发个消息我就给你结果

短任务

线程池里面有任务就拿,没任务就休眠,拿到了就处理任务

至此呢让线程池就把任务处理起来了。

task任务里的run方法 这次服务服务完了就算完了,就得把文件描述符关了,不关就会卡在这里,最终我们想让它直接就退出的。

close之后如图,相当于向服务器发起请求,服务器把结果给你,连接关闭,理解成线程池专门给统一处理任务。

应用-简单的翻译系统

我们的翻译系统就是打开一个文件,这个文件是如此KV结构

弄一个新的任务类init,红色是C++打开文件流和关闭流

黑色是getline按行读取,从定义的流in里面读,split是封装函数把这个kv切开,然后把part1 和part2放进map里,方便我们查询翻译

getline套循环是什么意思?

其实是返回布尔类型去判断的,它内部对强制类型转化符做运算符重载,如果getline读取字符到文件结束了,它直接返回空,循环就结束了。

.分割是如此分割的,基本功

这样的话把Init对象放到task类里面,虽然这里不太好,但是先这样放

这个对象一构造,KV的map也就好了

在Init里面还要有一个翻译的方法,方便run方法调用 用key查Value

就是通过map的接口一查

我们用telnet去输入apple 翻译不出来苹果

telnet的行分隔符会导致我们的翻译查不出来,行分隔符不一定是一个还有可能是两个。

所以要把客户端发来的数据去掉分隔符 buffer [n-2]

翻译成功

翻译就是用一次给你一次,相当于有在线翻译了。

服务器细节

write 返回值

写了多少字节返回值就是几,失败了-1被返回,错误码被设置。

我们这里做差错处理就可以,往fd=100不存的文件描述符写让他报错

但如果你刚把数据读上来(read刚刚结束,write还没开始),客户端就把连接关闭了,此时我在向这个文件写write会怎么样?

我们就想知道向一个已经断开的连接里写入会出什么问题

于是在中间close(sockfd)

或者不加close就让客户端直接退了,但是实验现象还是没做出来,现象就是卡住了

结论

TCP协议,偶发会出现,当我们向一个底层连接被双方释放掉的文件描述符写入的话,程序就好比管道进程通信,读端关闭,写端继续写的话当前进程就会收到SIGPIPE信号,OS直接干掉写端进程。

这种情况在网络里也可能存在,所以为了服务器安全你不仅要处理读(read),你也要处理写,所以写write 的 差错处理你做了

这个没问题。

可是wrtie写时也有可能出现写崩溃问题,所以代码层面上还要再加一个细节。

让服务器启动的时候,进行signal(SIGPIPE,SIG_IGN);

为了防止出现服务期向一个已经被关闭的文件描述符写入时,此时就像管道读端被关闭,写端继续写,连接也就没有意义了,OS就会直接把你的进程SIGPIPE 杀掉。

所以服务器建议

  1. 忽略SIGPIPE
  2. 对write进行差错处理,虽然什么都没做但是你得打出错误消息

客户端

简单翻译系统情况下,进程池提供短时服务,翻译服务完成直接就关闭文件描述符,连接也就断了,客户端这边也就卡住了

今天程序已经变了,把代码再改改,服务器每次服务完把连接就断了,所以我把循环提到connect上面,相当于每次进行翻译时都要重新建立链接,因为服务器只会给我提供一次服务。

在客户端write最后也做一次判断

我们完成了一次请求,可是第二次connect失败了,主要原因是,我们发起一次请求后,对方把我们对应整个的套接字关掉了,你下次再拿这个套接字的话,当前客户端对应的资源也已经被释放掉了,发起连接时会创建套接字,创建套接字时需要对信息重新绑定,你可以理解为这个文件sockfd也被对方释放掉了。

所以要没问题,还要继续把while循环往上提,提到socket调用之前

此时每一次服务器把连接关掉完成一次服务,重新创建套接字,重新发起连接,填充sockaddr_in结构体字段可以不用每次循环都填入,放到循环外面,因为里面只有类型,ip,端口,都是循环外可以做的。

此时客户端就可以不断的发起请求

重连过程中你得知道是网络出问题了,你得先让他写出错,或读出错,说明网络出问题了,所以交给上面让上面去重连

客户端read之前,如果服务器挂掉,那么客户端read也就读到0了。

读端如果没了,写端就会失败

客户端write之前,如果服务器挂掉,write返回小于0,但这个现象在客户端没实验出来。待定

不过没关系,反正连接断开,最后都会反映到connect返回值失败的差错判断里

后面就是常规提供服务过程,最开始加了重连模块,正常情况下套接字被创建,连接发起,都没问题,cnt 和 isreconnect只有后者是falsed ,所以直接继续往后走提供服务,但如果重连时,创建套接字这个在你本地工作肯定能成功,但是connnect总是连不上,那就要让他do while再去connect。

socket文件的处理放到do while外面,只需要处理connect。

如果重连五次还是不对,循环自动结束,客户端也就离线了。

就像我们打游戏要重连,这个重连工作是由客户端来做的。

下面这个服务可以改为长时服务,循环里面再套循环,有的服务就是长连接。

服务里面的read 和 write 失败了就要break,我们就认为是异常退出了 ,此时就重连了。

客户端不会收到管道信号吗? 也可能

你客户端会不会崩溃掉啊,也有不响应吧。

服务器端不会轻易把客户端怎么样,所以如果真正出问题给客户暴露出来,客户端对于SIGPIPE看情况忽略

守护进程化

前台和后台进程的原理

linux系统里,在进行登录的时候,当一个用户登录时linux系统会形成一次会话session,今天张三登录形成一个会话,李四登录也会再为李四形成一个会话,每一次登录都会有一个会话。

讲信号时,进程运行时默认执行命令它什么反应都没有,因为每一次登录时,每一个会话在OS里都会为每一次登录的会话创建一个bash进程

相当于每一个人登录时,默认就有一个bash,这个bash就可以为张三李四提供命令行服务。其中默认下一次会话默认只有一个bash进程,这个bash和键盘显示器直接相关,所以张三就能拿着键盘向bash输入,bash就能命令行解释,把解释的结果通过显示器打印出来。

所以ctrl +c就能终止前台进程

我们也可以./porcess & 以后台任务的形式把它放在后台

此时发现ls pwd指令又能执行了,虽然输出打乱了。而ctrl+c无法终止后台进程

第二个事实,在一次登录会话中,一个会话中只能存在一个前台进程,允许存在多个后台进程。

一个session,只能有一个前台进程在运行,键盘信号只能发给前台进程,后台进程是不鸟你的。

稍后查一下他们确实属于同一个会话的。

可是为什么前台进程执行命令就没有反应呢?

你自己执行程序时,OS会把bash自动投递到后台,你的进程就变成前台进程了,所以ctrl+c就能终止,所以ls pwd没反应,因为ls pwd是有Bash来解释的。

你会发现一旦你把自己的进程干掉了,我们必须得有前台进程,所以OS会自动把bash进程再移动到前台,所以他再给你提供命令行解释服务。

所以

什么叫做前台,什么叫做后台呢?

不论是前台还是后台,他都可以向显示器打印,为什么要用后台呢,可能进程比较耗时,我就把它放到后台,

默认你不把进程输出重定向到文件中,最后不管是前台还是后台都会往显示器去打印,

说明显示器并不是区分前台后台的重要指标,谁才是呢?

所以

什么叫做前台,什么叫做后台呢?

本质就是谁拥有键盘文件,谁就是前台

也就是后台进程不能进行标准输入。

你的电脑只有一个键盘,所以只有一个前台进程

OS允不允许你从键盘里读数据,会话里这么多进程,到底这个键盘输入的hello要交给哪一个进程呢?

OS就说谁拥有键盘文件,就把消息给谁。

jobs 显示后台

fg 任务号 切回前台

ctrl+z 把前台进程直接暂停,给进程发送19号SIGSTOP信号,自动转为后台

我们把前台进程直接暂停了,系统要不要把BASH自动提到前台,把暂停的进程自动提到后台??

要的,为什么?

系统必须把bash提到前台,没有人顶bash就得顶。

在命令行中,前台进程一直要存在。

如果会话中全是后台,键盘敲烂整个会话服务不会有任何反应。

为什么要把前台暂停的进程放到后台?

你把自己的前台进程暂停了,也就不会被调度,不会输入输出,为什么还要让你占着前台的键盘资源,我赶紧把资源让出来,让bash放到前台。

可是我想让这个后台暂停任务继续跑起来

,别老是暂停着呢。

bg 任务号 background 就继续开始跑了

Linux的进程间关系

每一个用户登录系统时都要形成一个session

十个用户登录就要形成10个会话

所以OS要不要把会话管理起来呀?

先描述在组织。

所以OS内核里会维护session的结构体,结构体里面就会包含会话对应的bash所对应的pid是什么,当前有多少前台后台,会话对应的终端是谁。

session很多,所以就要给每个session都要起一个SID(session id),用来在系统层面上进行区分

process的pid = 24314

他的PGID = 24314

说明他是自称进程组的

sleep 100 | sleep 100 | sleep 100 &

只是做对比

三个sleep进程pid都不一样

但是他们的PGID是24627 是第一个sleep的pid,说明他们三个自成一组,组长是谁呢?是多个进程中的第一个。

我们以多个进程中的第一个进程的pid来充当组id,所以这几个进程称之为进程组。

换句话说在linux中进程和进程的关系不仅仅有进程独立性的关系,进程和进程也可以互相构成一个组哦。

进程组和任务有什么关系呢?

以前进程就叫做任务,所以进程控制块叫task_struct

举个例子

包工头要把小区垃圾清理,盖电动车棚子

把小区垃圾清理交给process一个人做,

盖棚子比较难,交给第二个进程组来做,由多进程互相协同完成任务。

任务代表具体的事情,可以由一个人完成,也可以由团队共同完成

所以进程组合任务的关系相当于任务要指派给进程组。

任务是偏用户的概念,OS中任务要由进程组去完成。

我们把bash叫做前台进程,后台的叫做后台进程,我们更愿意把它教程前台任务和后台任务。

因为后台任务里可能会包含多个进程,他们是属于一块的。

所以大部分情况下,很多进程都是自成进程组的。

无论是process还是三个sleep,他们都属于同一个会话,他们的会话ID SID都是不变的。

多个任务(进程组),在同一个session内启动的,sid是一样的。

最后1351又是谁呢?

对应的bash pid命名构建一个session

登录的时候是这样,退出的时候呢???看后续

相关推荐
快乐飒男9 分钟前
Linux基础05
linux·笔记·学习
像素之间22 分钟前
`URL.createObjectURL(blob)` 和 `URL` 对象的区别
前端·javascript·网络
.Ayang44 分钟前
微服务介绍
网络·安全·网络安全·微服务·云原生·架构·安全架构
Who_Mr.Lin1 小时前
【虚拟机】VMWare的CentOS虚拟机断电或强制关机出现问题
linux·运维·centos
我是唐青枫1 小时前
Linux nc 命令详解
linux·运维·服务器
网安墨雨1 小时前
网络安全笔记
网络·笔记·web安全
南东山人1 小时前
关于内核编程的一些笔记
linux·笔记
ejinxian1 小时前
Windows 系统上构建 Linux 应用
linux·运维·服务器·red hat
Java 第一深情2 小时前
详细教程-Linux上安装单机版的Hadoop
linux·运维·hadoop
Hello_wshuo2 小时前
/proc/sys/net/ipv4/ip_forward 被关闭问题排查
网络·网络协议·tcp/ip