socket是并发安全的吗 2

简介: socket是并发安全的吗



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

最后

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


最近原创更文的阅读量稳步下跌,思前想后,夜里辗转反侧。

我有个不成熟的请求。

离开广东好长时间了,好久没人叫我靓仔了。

大家可以在评论区里,叫我一靓仔吗?

别说了,一起在知识的海洋里呛水吧

目录
相关文章
|
8月前
|
安全 网络协议 Linux
socket是并发安全的吗 1
socket是并发安全的吗
94 0
|
缓存 负载均衡 网络协议
【Socket】两种高效事件处理模式&并发模式
【Socket】两种高效事件处理模式&并发模式
|
Linux
epoll+socket实现 socket并发 linux服务器
/* 实现功能:通过epoll, 处理多个socket * 监听一个端口,监听到有链接时,添加到epoll_event * xs */ #include #include #include #include ...
2511 0
|
网络协议 Java Apache
大并发量 socket 通信的解决方案
大并发量 socket 通信的解决方案http://www.bieryun.com/1537.html 笔者之前的工作主要是做java的web端开发,后因工作原因参与了一个国家级的大项目,主要负责其中底层通讯的前置机模块。
3351 0
|
监控 测试技术 C#
C#高性能大容量SOCKET并发(一):IOCP完成端口例子介绍
原文:C#高性能大容量SOCKET并发(一):IOCP完成端口例子介绍 例子主要包括SocketAsyncEventArgs通讯封装、服务端实现日志查看、SCOKET列表、上传、下载、远程文件流、吞吐量协议,用于测试SocketAsyncEventArgs的性能和压力,最大连接数支持65535个长连接,最高命令交互速度达到250MB/S(使用的是127.0.0.1的方式,相当于千兆网卡1Gb=125MB/S两倍的吞吐量)。
3168 0
|
C# 缓存
C#高性能大容量SOCKET并发(二):SocketAsyncEventArgs封装
原文:C#高性能大容量SOCKET并发(二):SocketAsyncEventArgs封装 1、SocketAsyncEventArgs介绍 SocketAsyncEventArgs是微软提供的高性能异步Socket实现类,主要为高性能网络服务器应用程序而设计,主要是为了避免在在异步套接字 I/O 量非常大时发生重复的对象分配和同步。
3392 0
Socket编程实践(4) --多进程并发server
1.Socket地址复用 int getsockopt(int sockfd, int level, int optname, void *optval, so...
974 0
|
17天前
|
安全 Java 数据处理
Python网络编程基础(Socket编程)多线程/多进程服务器编程
【4月更文挑战第11天】在网络编程中,随着客户端数量的增加,服务器的处理能力成为了一个重要的考量因素。为了处理多个客户端的并发请求,我们通常需要采用多线程或多进程的方式。在本章中,我们将探讨多线程/多进程服务器编程的概念,并通过一个多线程服务器的示例来演示其实现。