首页 » SEO优化 » phpmemcache加锁队列技巧_socket是并发安然的吗

phpmemcache加锁队列技巧_socket是并发安然的吗

访客 2024-11-15 0

扫一扫用手机浏览

文章目录 [+]

我相信我读者大部分都是做互联网运用开拓的,可能对游戏的架构不太理解。

我们想象中的游戏架构是下面这样的。

phpmemcache加锁队列技巧_socket是并发安然的吗

想象中的游戏架构

phpmemcache加锁队列技巧_socket是并发安然的吗
(图片来自网络侵删)

也便是用户客户端直接连接游戏核心逻辑做事器,下面简称GameServer。
GameServer紧张卖力实现各种玩法逻辑。

这当然是能跑起来,实现也很大略。

但这样会有个问题,由于游戏这块蛋糕很大,以是总会碰着很多挺刑的事情。

如果让用户直连GameServer,那相称于把GameServer的ip暴露给了所有人。

不赢利还好,一旦游戏赢利,就会碰着各种攻击。

你猜《羊了个羊》最火的时候为啥总是崩溃?

假设一个游戏做事器能承载4k玩家,一旦做事器遭受直接攻击,那4k玩家都会被影响。

这攻击的是做事器吗?这明明攻击的是老板的钱包。

以是很多时候不会让用户直连GameServer。

而是在前面加入一层网关层,下面简称gateway。
类似这样。

GameServer就躲在了gateway背后,用户只能得到gateway的IP。

然后将大概每100个用户放在一个gateway里,这样如果真被攻击,就算gateway崩了,受影响的也就那100个玩家。

由于大部分游戏都利用TCP做开拓,以是下面提到的连接,如果没有特殊解释,那都是指TCP连接。

那么问题来了。

假设有100个用户连gateway,那gateway跟GameServer之间也会是 100个连接吗?

当然不会,gateway跟GameServer之间的连接数会远小于100。

由于这100个用户不会一贯须要收发,总有空闲的时候,完备可以让多个用户复用同一条连接,将数据打包一起发送给GameServer,这样单个连接的利用率也高了,GameServer 也不再须要同时坚持太多连接,可以节省了不少资源,这样就可以多做事几个大怨种金主。

我们知道,要对网络连接写数据,就要实行 send(socket_fd, data)。

于是问题就来了。

已知多个用户共用同一条连接。

现在多个用户要发数据,也便是多个用户线程须要写同一个socket_fd。

那么,socket是并发安全的吗?能让这多个线程同时并发写吗?

并发读写socket

写TCP Socket是线程安全的吗?

对付TCP,我们一样平常利用下面的办法创建socket。

sockfd=socket(AF_INET,SOCK_STREAM, 0))

返回的sockfd是socket的句柄id,用于在全体操作系统中唯一标识你的socket是哪个,可以理解为socket的身份证id。

创建socket时,操作系统内核会顺带为socket创建一个发送缓冲区和一个吸收缓冲区。
分别用于在发送和吸收数据的时候给暂存一下数据。

写socket的办法有很多,既可以是send,也可以是write。

但不管哪个,末了在内核里都会走到 tcp_sendmsg() 函数下。

// net/ipv4/tcp.cint tcp_sendmsg(struct kiocb iocb, struct sock sk, struct msghdr msg, size_t size){ // 加锁 lock_sock(sk); // ... 拷贝到发送缓冲区的干系操作 // 解锁 release_sock(sk);}

在tcp_sendmsg的目的便是将要发送的数据放入到TCP的发送缓冲区中,此时并没有所谓的发送数据出去,函数就返回了,内核后续再根据实际情形异步发送。
关于这点,我在之前写过的 《动图图解 | 代码实行send成功后,数据就发出去了吗?》有更详细的先容。

从tcp_sendmsg的代码中可以看到,在对socket的缓冲区实行写操作的时候,linux内核已经自动帮我们加好了锁,也便是说,是线程安全的。

以是可以多线程不加锁并发写入数据吗?

不能。

问题的关键在于锁的粒度。

但我们知道TCP有三大特点,面向连接,可靠的,基于字节流的协议。

问题就出在这个"基于字节流",它是个源源不断的二进制数据流,无边界。
来多少就发多少,但是能发多少,得看你的发送缓冲区还剩多少空间。

举个例子,假设A线程想发123数据包,B线程想发456数据包。

A和B线程同时实行send(),A先抢到锁,此时发送缓冲区就剩1个数据包的位置,那发了"1",然后发送缓冲区满了,A线程退出(非壅塞),当发送缓冲区腾出位置后,此时AB再次同时争抢,这次被B先抢到了,B发了"4"之后缓冲区又满了,不得不退出。

重复这样多次争抢之后,原来的数据内容都被打乱了,变成了142356。
由于数据123是个整体,456又是个整体,像现在这样数据被打乱的话,吸收方就算收到了数据也没办法正常解析。

并发写socket_fd导致数据非常

也便是说锁的粒度实在是每次"写操作",但每次写操作并不担保能把写完全。

那么问题就来了,那是不是我在写全体完全之前加个锁,全体都写完之后再解锁,这样就好了?

类似下面这样。

// 伪代码int safe_send(msg string){ target_len = length(msg) have_send_len = 0 // 加锁 lock(); // 不断循环直到发完全个完全 do { send_len := send(sockfd,msg) have_send_len = have_send_len + send_len } while(have_send_len < target_len) // 解锁 unlock();}

这也弗成,我们知道加锁这个事情是影响性能的,锁的粒度越小,性能就越好。
反之性能就越差。

当我们抢到了锁,利用 send(sockfd,msg) 发送完全数据的时候,如果此时发送缓冲区恰好一写就满了,那这个线程就得一贯占着这个锁直到全体写完。
其他线程都在阁下等它解锁,啥事也干不了,发急难耐想着抢锁。

但凡某个别轻微大点,这样的问题就会变得更严重。
全体做事的性能也会被这波神仙操作给拖垮。

归根结底还是由于锁的粒度太大了。

有没有更好的办法呢?

实在多个线程抢锁,末了抢到锁的线程才能进行写操作,从实质上来看,便是将所有用户发给GameServer逻辑做事器的给串行化了,

那既然是串行化,我完备可以在在业务代码里为每个socket_fd配一个行列步队来做,将数据在用户态加锁后塞到这个行列步队里,再单独开一个线程,这个线程的事情便是发送给socket_fd。

于是上面的场景就变成了下面这样。

并发写到加锁行列步队后由一个线程处理

于是在gateway层,多个用户线程同时写时,会去争抢某个socket_fd对应的行列步队,抢到锁之后就写数据到行列步队。
而真正实行 send(sockfd,msg) 的线程实在只有一个。
它会从这个行列步队中取数据,然后不加锁的批量发送数据到 GameServer。

由于加锁后要做的事情很大略,也就塞个行列步队而已,因此非常快。
并且由于实行发送数据的只有单个线程,因此也不会有体乱序的问题。

读TCP Socket是线程安全的吗?

在前面有了写socket是线程安全的结论,我们轻微翻一下源码就能创造,读socket实在也是加锁了的,以是并发多线程读socket这件事是线程安全的。

// net/ipv4/tcp.cint tcp_recvmsg(struct kiocb iocb, struct sock sk, struct msghdr msg, size_t len, int nonblock, int flags, int addr_len){ // 加锁 lock_sock(sk); // ... 将数据从吸收缓冲区拷贝到用户缓冲区 // 开释锁 release_sock(sk);}

但就算是线程安全,也不代表你可以用多个线程并发去读。

由于这个锁,只担保你在读socket 吸收缓冲区时,只有一个线程在读,但并不能担保你每次的时候,都能恰好读到完全部后才返回。

以是虽然并发读不报错,但每个线程拿到切实其实建都不全,由于锁的粒度并不担保能读完完全。

TCP是基于数据流的协议,数据流会源源不断从网卡那送到吸收缓冲区。

如果此时吸收缓冲区里有两条完全,比如 "我是小白"和"点赞在看走一波"。

有两个线程A和B同时并发去读的话,A线程就可能读到“我是 点赞走一波", B线程就可能读到”小白 在看"

两条都变得不完全了。

并发读socket_fd导致的数据非常

办理方案还是跟读的时候一样,读socket的只能有一个线程,读到了之后塞到加锁行列步队中,再将分开给到GameServer的多线程用户逻辑模块中去做处理。

单线程读socket_fd后写入加锁行列步队

读写UDP Socket是线程安全的吗?

聊完TCP,我们很自然就能想到其余一个传输层协议UDP,那么它是线程安全的吗?

我们平时写代码的时候如果要利用udp发送,一样平常会像下面这样操作。

ssize_t sendto(int sockfd, const void buf, size_t nbytes, int flags, const struct sockaddr to, socklen_t addrlen);

而实行到底层,会到linux内核的udp_sendmsg函数中。

int udp_sendmsg(struct kiocb iocb, struct sock sk, struct msghdr msg, size_t len) { if (用到了MSG_MORE的功能) { lock_sock(sk); // 加入到发送缓冲区中 release_sock(sk); } else { // 不加锁,直接发送 }}

这里我用伪代码改了下,大概的含义便是用到MSG_MORE就加锁,否则不加锁将传入的msg作为一全体数据包直接发送。

首先须要搞清楚,MSG_MORE 是啥。
它可以通过上面提到的sendto函数最右边的flags字段进行设置。
大概的意思是见告内核,待会还有其他更多要一起发,先别焦急发出去。
此时内核就会把这份数据先用发送缓冲区缓存起来,待会运用层说ok了,再一起发。

但是,我们一样平常也用不到 MSG_MORE。

以是我们直接关注其余一个分支,也便是不加锁直接发。

那是不是解释走了不加锁的分支时,udp发并不是线程安全的?

实在。
还是线程安全的,不用lock_sock(sk)加锁,纯挚是由于没必要。

开启MSG_MORE时多个线程会同时写到同一个socket_fd对应的发送缓冲区中,然后再统一一起发送到IP层,因此须要有个锁防止涌现多个线程将对方写的数据给覆盖掉的问题。
而不开启MSG_MORE时,数据则会直接发送给IP层,就没有了上面的烦恼。

再看下udp的吸收函数udp_recvmsg,会创造情形也类似,这里就不再赘述。

能否多线程同时并发读或写同一个UDP socket?

在TCP中,线程安全不代表你可以并发地读写同一个socket_fd,由于哪怕内核态中加了lock_sock(sk),这个锁的粒度并不覆盖全体完全的多次分批发送,它只担保单次发送的线程安全,以是建议只用一个线程去读写一个socket_fd。

那么问题又来了,那UDP呢?会有一样的问题吗?

我们跟TCP比拟下,大家就知道了。

TCP不能用多线程同时读和同时写,是由于它是基于数据流的协议。

那UDP呢?它是基于数据报的协议。

基于数据流和基于数据报有什么差异呢?

基于数据流,意味着发给内核底层的数据就跟水进入水管一样,内核根本不知道什么时候是个头,没有明确的边界。

而基于数据报,可以类比为一件件快递进入传送管道一样,内核很清楚拿到的是几件快递,快递和快递之间边界分明。

水点和快递的差异

那从我们利用的办法来看,运用层通过TCP去发数据,TCP就先把它放到缓冲区中,然后就返回。
至于什么时候发数据,发多少数据,发的数据是刚刚运用层传进去的一半还是全部都是不愿定的,全看内核的心情。
在吸收端收的时候也一样。

但UDP就不同,UDP 对运用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。

无论运用层交给 UDP 多长的报文,UDP 都还是发送,即一次发送一个报文。
至于数据包太长,须要分片,那也是IP层的事情,跟UDP没啥关系,大不了效率低一些。
而吸收方在吸收数据报的时候,一次取一个完全的包,不存在TCP常见的半包和粘包问题。

正由于基于数据报和基于字节流的差异,TCP 发送端发 10 次字节流数据,吸收端可以分 100 次去取数据,每次取数据的长度可以根据处理能力作调度;但 UDP 发送端发了 10 次数据报,那吸收端就要在 10 次收完,且发了多少次,就取多少次,确保每次都是一个完全的数据报。

以是从这个角度来说,UDP写数据报的行为是"原子"的,不存在发一半包或收一半包的问题,要么全体包成功,要么全体包失落败。
因此多个线程同时读写,也就不会有TCP的问题。

以是,可以多个线程同时读写同一个udp socket。

但就算可以,我依然不建议大家这么做。

为什么不建议利用多线程同时读写同一个UDP socket

udp本身是不可靠的协议,多线程高并发实行发送时,会对系统造成较大压力,这时候丢包是常见的事情。
虽然这时候运用层能实现重传逻辑,但重传这件事毕竟是越少越好。
因此常日还会希望能有个运用层流量掌握的功能,如果是单线程读写的话,就可以在同一个地方对流量实现调控。
类似的,实现其他插件功能也会更加方便,比如给某些vip等级的老板更快速的游戏体验啥的(我瞎说的)。

以是精确的做法,还是跟TCP一样,不管表面有多少个线程,还是并发加锁写到一个行列步队里,然后起一个单独的线程去做发送操作。

udp并发写加锁行列步队后再写socket_fd

总结多线程并发读/写同一个TCP socket是线程安全的,由于TCP socket的读/写操作都上锁了。
虽然线程安全,但依然不建议你这么做,由于TCP本身是基于数据流的协议,一份完全的数据可能会分开多次去写/读,内核的锁只担保单次读/写socket是线程安全,锁的粒度并不覆盖全体完全。
因此建议用一个线程去读/写TCP socket。
多线程并发读/写同一个UDP socket也是线程安全的,由于UDP socket的读/写操作也都上锁了。
UDP写数据报的行为是"原子"的,不存在发一半包或收一半包的问题,要么全体包成功,要么全体包失落败。
因此多个线程同时读写,也就不会有TCP的问题。
虽然如此,但还是建议用一个线程去读/写UDP socket。
末了

上面文章里提到,建议用单线程的办法去读/写socket,但每个socket都配一个线程这件事情,显然有些奢侈,比如线程切换的代价也不小,那这种情形有什么好的办理办法吗?

https://mp.weixin.qq.com/s/rNfBHtpFLxwY7-CiBvkQ5A

标签:

相关文章

QQ聊天恶搞代码技术背后的趣味与风险

人们的生活越来越离不开社交软件。在我国,QQ作为一款历史悠久、用户众多的社交平台,深受广大网民喜爱。在QQ聊天的过程中,恶搞代码的...

SEO优化 2025-03-02 阅读1 评论0

Python代码截屏技术与应用的完美融合

计算机屏幕截图已经成为人们日常生活中不可或缺的一部分。无论是分享工作成果、记录游戏瞬间,还是保存网页信息,屏幕截图都发挥着重要作用...

SEO优化 2025-03-02 阅读1 评论0

QQ无限刷礼物代码技术突破还是道德沦丧

社交平台逐渐成为人们生活中不可或缺的一部分。QQ作为我国最具影响力的社交软件之一,其丰富的功能吸引了大量用户。近期有关QQ无限刷礼...

SEO优化 2025-03-02 阅读1 评论0