面试加分题--socket是否是并发安全的?

news2025/1/15 23:22:56

今天和大家聊一个有点儿东西的面试题: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.c
int tcp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg, size_t size)
{
    // 加锁
    lock_sock(sk);


    // ... 拷贝到发送缓冲区的相关操作


    // 解锁
    release_sock(sk);
}

tcp_sendmsg的目的就是将要发送的数据放入到 TCP 的发送缓冲区中,此时并没有所谓的发送数据出去,函数就返回了,内核后续再根据实际情况异步发送。

tcp_sendmsg 逻辑

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

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

不能。

问题的关键在于锁的粒度

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

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.c
int 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呢?它是基于数据报的协议。

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

总结

1. 多线程并发读/写同一个 TCP socket 是线程安全的,因为 TCP socket 的读/写操作都上锁了。虽然线程安全,但依然不建议你这么做,因为TCP本身是基于数据流的协议,一份完整的消息数据可能会分开多次去写/读,内核的锁只保证单次读/写socket是线程安全,锁的粒度并不覆盖整个完整消息。因此建议用一个线程去读/写 TCP socket。

2. 多线程并发读/写同一个 UDP socket 也是线程安全的,因为UDP socket的读/写操作也都上锁了。UDP 写数据报的行为是"原子"的,不存在发一半包或收一半包的问题,要么整个包成功,要么整个包失败。因此多个线程同时读写,也就不会有 TCP 的问题。虽然如此,但还是建议用一个线程去读/写 UDP socket。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/162480.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

解决⾃动驾驶中计算机视觉的⽬标检测问题

来源&#xff1a;投稿 作者&#xff1a;cairuyi01 编辑&#xff1a;学姐 最近读了《Object detection with location-aware deformable convolution and backward attention filtering》&#xff0c;这是⼀篇2019年刊登在CVPR上的CV论⽂。与解决普适性的CV任务不同&#xff0c…

SpringMVC如何优化Ajax技术

SpringMVC如何优化Ajax技术&#xff1f; AJAX Asynchronous JavaScript and XML&#xff08;异步的 JavaScript 和 XML&#xff09;。 AJAX 是一种在无需重新加载整个网页的情况下&#xff0c;能够更新部分网页的技术。 Ajax 不是一种新的编程语言&#xff0c;而是一种用于创…

EIoU和Focal-EIoU Loss

1、论文 论文题目&#xff1a;《Focal and Efficient IOU Loss for Accurate Bounding Box Regression》 2、引言 CIoU Loss虽然考虑了边界框回归的重叠面积、中心点距离、高宽比。但是其公式中的v反映的是高宽的差异&#xff0c;而不是高宽分别与其置信度的真实差异。因此&…

蚂蚁智能内容合规审核产品探秘

随着互联网服务的不断深化&#xff0c;产品营销的形式从传统文本、长图文&#xff0c;增加到短视频、直播等新媒介形态&#xff0c;展现形式愈加丰富的同时&#xff0c;也为营销宣传内容合规审核带来了诸多难题。如何解决与日俱增的审核量与合规审核人员有限之间的矛盾&#xf…

【阶段三】Python机器学习31篇:机器学习项目实战:基于皮尔逊相关系数搭建电影智能推荐系统

本篇的思维导图: 项目背景 在当今这个大数据时代,智能推荐系统的应用越来越广泛,网上购物、在线观影、新闻推送的背后都有智能推荐系统算法的支持。人们经常会在视频平台上观看电影,有时明确想要观看某部电影,有时则仅仅是随机搜寻。如果视频平台能利用基于物品的…

DDOS攻击

把我掘金的文章同步一份过来 最近网上爆火的一款游戏 Goose Goose Duck (鹅鸭杀) 游戏官方在近日发布了一则公告&#xff0c;宣布由于服务器屡次遭受黑客攻击&#xff0c;该游戏服务器将暂时关服三天进行维护 遭到了DDOS攻击&#xff0c;背后原因&#xff0c;我们不做讨论&…

代码随想录算法训练营第十七天二叉树 java : . 110.平衡二叉树 257.二叉树的所有路径 404.左叶子之和

文章目录前言Leetcode 110.平衡二叉树题目讲解思路Leetcode 257. 二叉树的所有路径题目讲解这道题涉及到了回溯Leetcode 404.左叶子之和题目讲解总结前言 选择一个简单的理念&#xff0c;矢志不渝地去执行&#xff08;Take one simple idea and take it seriously 递归三部曲…

【Nginx】Nginx搭建高可用集群

1. KeepalivedNginx 高可用集群&#xff08;主从模式&#xff09;2. 配置高可用的准备工作3. 在两台服务器上安装keepalived4. 完成高可用配置(主从配置)5. 最终测试 1. KeepalivedNginx 高可用集群&#xff08;主从模式&#xff09; 2. 配置高可用的准备工作 需要两台服务器…

Revit如何将明细表导出为DWG格式【批量导出图纸】

一、Revit中怎样将明细表导出到DWG文件中 有时需要将Revit中生成的各种明细表导入到CAD中使用&#xff0c;但是在明细表视图中并没有导出成DWG格式的选项如图1所示&#xff0c;应该如何操作才能导出成CAD可识别文件呢&#xff1f; 方法一&#xff1a;将明细表通过导出为报表选项…

Java 核心技术卷 I 基础知识笔记(一)

Java 的基本程序设计结构 2.1 一个简单的 Java 应用程序 一个最简单的 Java 应用程序&#xff0c;它只发送一条消息到控制台窗口中&#xff1a;/*** This is the first sample program in Core Java Chapter 3* version 1.01 1997-03-22* author Gary Cornell*/ public class…

分享111个Java源码,总有一款适合您

Java源码 分享111个Java源码&#xff0c;总有一款适合您 源码下载链接&#xff1a;https://pan.baidu.com/s/1fycjYHA7y6r-IH8H7v5XKA?pwdag8l 提取码&#xff1a;ag8l 下面是文件的名字&#xff0c;我放了一些图片&#xff0c;文章里不是所有的图主要是放不下...&#xff…

网络编程学习记录

服务端首先是确定协议版本。首先定义一个结构体 WSADATA wsadata; 这个结构体是啥呢&#xff1f; 是Windows下得到广泛应用的、开放的、支持多种协议的网络编程接口。大家晓得了吧。 让我们看看这个结构体。 typedef struct WSAData {WORD wVersion; …

c++ socket之io复用模型 epoll进阶

服务器开发系列 文章目录服务器开发系列前言一、socket epoll介绍二、代码实现1. epoll client实现2. epoll server实现3. epoll client server验证总结前言 I/O复用模型&#xff1a;主要是指&#xff0c;一个线程可以同时监控多个系统IO、并且能够操作多个系统IO的一种技术模…

西瓜书第一章课后题答案(一)

1.1 针对西瓜分类分题进行讲解属性&#xff1a; 3个属性色泽&#xff1a;&#xff08;青绿&#xff0c;乌黑&#xff0c;浅白&#xff09;根蒂&#xff1a;&#xff08;蜷缩&#xff0c;硬挺&#xff0c;稍蜷&#xff09;敲声&#xff1a;&#xff08;浊响&#xff0c;清脆&…

Docker网络network详解

一、概述 Docker容器每次重启后容器ip是会发生变化的。 这也意味着如果容器间使用ip地址来进行通信的话&#xff0c;一旦有容器重启&#xff0c;重启的容器将不再能被访问到。 而Docker 网络就能够解决这个问题。 Docker 网络主要有以下两个作用&#xff1a; 容器间的互联和…

【ROS2入门】理解 ROS 2 节点

大家好&#xff0c;我是虎哥&#xff0c;从今天开始&#xff0c;我将花一段时间&#xff0c;开始将自己从ROS1切换到ROS2&#xff0c;在上一篇中&#xff0c;我们依托Turtlesim演示节点来逐步展开&#xff0c;介绍了rqt工具&#xff0c;这一章&#xff0c;我们将围绕ROS2中主要…

jvm快速入门

1.JVM介绍 1.什么是jvm Java Virtual Machine&#xff08;java二进制字节码运行环境&#xff09; 好处&#xff1a; 一次编译&#xff0c;好处运行自动内存管理&#xff0c;垃圾回收机制数组下标越界检查多态 比较JVM\JRE\JDK jvm屏蔽java代码与底层操作系统的差异 JREJVM基…

基于 java springboot+layui仓库管理系统设计和实现

基于 java springbootlayui仓库管理系统设计和实现 博主介绍&#xff1a;5年java开发经验&#xff0c;专注Java开发、定制、远程、文档编写指导等,csdn特邀作者、专注于Java技术领域 作者主页 超级帅帅吴 Java毕设项目精品实战案例《500套》 欢迎点赞 收藏 ⭐留言 文末获取源码…

基于基于jsp+mysql+Spring+mybatis的SSM汽车保险理赔管理系统设计和实现

基于基于jspmysqlSpringmybatis的SSM汽车保险理赔管理系统设计和实现 博主介绍&#xff1a;5年java开发经验&#xff0c;专注Java开发、定制、远程、文档编写指导等,csdn特邀作者、专注于Java技术领域 作者主页 超级帅帅吴 Java毕设项目精品实战案例《500套》 欢迎点赞 收藏 ⭐…

12图、网络、关联矩阵

第 12 讲 图、网络、关联矩阵 Graphs&#xff0c;networks&#xff0c;incidence matrices 本讲讨论线性代数在物理系统中的应用。 图和网络 Graphs & Networks “图”就是“结点”和“边”的一个集合。 边线上的箭头代表从结点流出的正方向。 关联矩阵&#xff08;I…