自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Socket是并發(fā)安全的嗎

網(wǎng)絡(luò) 網(wǎng)絡(luò)管理
多線程并發(fā)讀/寫同一個(gè)TCP socket是線程安全的,因?yàn)門CP socket的讀/寫操作都上鎖了。雖然線程安全,但依然不建議你這么做,因?yàn)門CP本身是基于數(shù)據(jù)流的協(xié)議,一份完整的消息數(shù)據(jù)可能會(huì)分開(kāi)多次去寫/讀,內(nèi)核的鎖只保證單次讀/寫socket是線程安全,鎖的粒度并不覆蓋整個(gè)完整消息。因此建議用一個(gè)線程去讀/寫TCP socket。

 為了更好的聊今天的話題,我們先假設(shè)一個(gè)場(chǎng)景。

我相信我讀者大部分都是做互聯(lián)網(wǎng)應(yīng)用開(kāi)發(fā)的,可能對(duì)游戲的架構(gòu)不太了解。

我們想象中的游戲架構(gòu)是下面這樣的。

圖片

想象中的游戲架構(gòu)

也就是用戶客戶端直接連接游戲核心邏輯服務(wù)器,下面簡(jiǎn)稱GameServer。GameServer主要負(fù)責(zé)實(shí)現(xiàn)各種玩法邏輯。

這當(dāng)然是能跑起來(lái),實(shí)現(xiàn)也很簡(jiǎn)單。

但這樣會(huì)有個(gè)問(wèn)題,因?yàn)橛螒蜻@塊蛋糕很大,所以總會(huì)遇到很多挺刑的事情。

如果讓用戶直連GameServer,那相當(dāng)于把GameServer的ip暴露給了所有人。

不賺錢還好,一旦游戲賺錢,就會(huì)遇到各種攻擊。

你猜《羊了個(gè)羊》最火的時(shí)候?yàn)樯独鲜潜罎ⅲ?/p>

假設(shè)一個(gè)游戲服務(wù)器能承載4k玩家,一旦服務(wù)器遭受直接攻擊,那4k玩家都會(huì)被影響。

這攻擊的是服務(wù)器嗎?這明明攻擊的是老板的錢包。

所以很多時(shí)候不會(huì)讓用戶直連GameServer。

而是在前面加入一層網(wǎng)關(guān)層,下面簡(jiǎn)稱gateway。類似這樣。

圖片

實(shí)際的某些游戲架構(gòu)

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

然后將大概每100個(gè)用戶放在一個(gè)gateway里,這樣如果真被攻擊,就算gateway崩了,受影響的也就那100個(gè)玩家。

由于大部分游戲都使用TCP做開(kāi)發(fā),所以下面提到的連接,如果沒(méi)有特別說(shuō)明,那都是指TCP連接。

那么問(wèn)題來(lái)了。

假設(shè)有100個(gè)用戶連gateway,那gateway跟GameServer之間也會(huì)是 100個(gè)連接嗎?

當(dāng)然不會(huì),gateway跟GameServer之間的連接數(shù)會(huì)遠(yuǎn)小于100。

因?yàn)檫@100個(gè)用戶不會(huì)一直需要收發(fā)消息,總有空閑的時(shí)候,完全可以讓多個(gè)用戶復(fù)用同一條連接,將數(shù)據(jù)打包一起發(fā)送給GameServer,這樣單個(gè)連接的利用率也高了,GameServer 也不再需要同時(shí)維持太多連接,可以節(jié)省了不少資源,這樣就可以多服務(wù)幾個(gè)大怨種金主。

我們知道,要對(duì)網(wǎng)絡(luò)連接寫數(shù)據(jù),就要執(zhí)行 send(socket_fd, data)。

于是問(wèn)題就來(lái)了。

已知多個(gè)用戶共用同一條連接。

現(xiàn)在多個(gè)用戶要發(fā)數(shù)據(jù),也就是多個(gè)用戶線程需要寫同一個(gè)socket_fd。

那么,socket是并發(fā)安全的嗎?能讓這多個(gè)線程同時(shí)并發(fā)寫嗎?

圖片

并發(fā)讀寫socket

寫TCP Socket是線程安全的嗎?

對(duì)于TCP,我們一般使用下面的方式創(chuàng)建socket。

sockfd=socket(AF_INET,SOCK_STREAM, 0))

返回的sockfd是socket的句柄id,用于在整個(gè)操作系統(tǒng)中唯一標(biāo)識(shí)你的socket是哪個(gè),可以理解為socket的身份證id。

創(chuàng)建socket時(shí),操作系統(tǒng)內(nèi)核會(huì)順帶為socket創(chuàng)建一個(gè)發(fā)送緩沖區(qū)和一個(gè)接收緩沖區(qū)。分別用于在發(fā)送和接收數(shù)據(jù)的時(shí)候給暫存一下數(shù)據(jù)。

寫socket的方式有很多,既可以是send,也可以是write。

但不管哪個(gè),最后在內(nèi)核里都會(huì)走到 tcp_sendmsg() 函數(shù)下。

// net/ipv4/tcp.c
int tcp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg, size_t size)
{
// 加鎖
lock_sock(sk);


// ... 拷貝到發(fā)送緩沖區(qū)的相關(guān)操作


// 解鎖
release_sock(sk);
}

在tcp_sendmsg的目的就是將要發(fā)送的數(shù)據(jù)放入到TCP的發(fā)送緩沖區(qū)中,此時(shí)并沒(méi)有所謂的發(fā)送數(shù)據(jù)出去,函數(shù)就返回了,內(nèi)核后續(xù)再根據(jù)實(shí)際情況異步發(fā)送。關(guān)于這點(diǎn),我在之前寫過(guò)的 《動(dòng)圖圖解 | 代碼執(zhí)行send成功后,數(shù)據(jù)就發(fā)出去了嗎?》有更詳細(xì)的介紹。

圖片

tcp_sendmsg 邏輯

從tcp_sendmsg的代碼中可以看到,在對(duì)socket的緩沖區(qū)執(zhí)行寫操作的時(shí)候,linux內(nèi)核已經(jīng)自動(dòng)幫我們加好了鎖,也就是說(shuō),是線程安全的。

所以可以多線程不加鎖并發(fā)寫入數(shù)據(jù)嗎?

不能。

問(wèn)題的關(guān)鍵在于鎖的粒度。

但我們知道TCP有三大特點(diǎn),面向連接,可靠的,基于字節(jié)流的協(xié)議。

圖片

TCP是什么

問(wèn)題就出在這個(gè)"基于字節(jié)流",它是個(gè)源源不斷的二進(jìn)制數(shù)據(jù)流,無(wú)邊界。來(lái)多少就發(fā)多少,但是能發(fā)多少,得看你的發(fā)送緩沖區(qū)還剩多少空間。

舉個(gè)例子,假設(shè)A線程想發(fā)123數(shù)據(jù)包,B線程想發(fā)456數(shù)據(jù)包。

A和B線程同時(shí)執(zhí)行send(),A先搶到鎖,此時(shí)發(fā)送緩沖區(qū)就剩1個(gè)數(shù)據(jù)包的位置,那發(fā)了"1",然后發(fā)送緩沖區(qū)滿了,A線程退出(非阻塞),當(dāng)發(fā)送緩沖區(qū)騰出位置后,此時(shí)AB再次同時(shí)爭(zhēng)搶,這次被B先搶到了,B發(fā)了"4"之后緩沖區(qū)又滿了,不得不退出。

重復(fù)這樣多次爭(zhēng)搶之后,原本的數(shù)據(jù)內(nèi)容都被打亂了,變成了142356。因?yàn)閿?shù)據(jù)123是個(gè)整體,456又是個(gè)整體,像現(xiàn)在這樣數(shù)據(jù)被打亂的話,接收方就算收到了數(shù)據(jù)也沒(méi)辦法正常解析。

圖片

并發(fā)寫socket_fd導(dǎo)致數(shù)據(jù)異常

也就是說(shuō)鎖的粒度其實(shí)是每次"寫操作",但每次寫操作并不保證能把消息寫完整。

那么問(wèn)題就來(lái)了,那是不是我在寫整個(gè)完整消息之前加個(gè)鎖,整個(gè)消息都寫完之后再解鎖,這樣就好了?

類似下面這樣。

// 偽代碼
int safe_send(msg string)
{
target_len = length(msg)
have_send_len = 0
// 加鎖
lock();

// 不斷循環(huán)直到發(fā)完整個(gè)完整消息
do {
send_len := send(sockfd,msg)
have_send_len = have_send_len + send_len
} while(have_send_len < target_len)


// 解鎖
unlock();

}

這也不行,我們知道加鎖這個(gè)事情是影響性能的,鎖的粒度越小,性能就越好。反之性能就越差。

當(dāng)我們搶到了鎖,使用 send(sockfd,msg) 發(fā)送完整數(shù)據(jù)的時(shí)候,如果此時(shí)發(fā)送緩沖區(qū)正好一寫就滿了,那這個(gè)線程就得一直占著這個(gè)鎖直到整個(gè)消息寫完。其他線程都在旁邊等它解鎖,啥事也干不了,焦急難耐想著搶鎖。

但凡某個(gè)消息體稍微大點(diǎn),這樣的問(wèn)題就會(huì)變得更嚴(yán)重。整個(gè)服務(wù)的性能也會(huì)被這波神仙操作給拖垮。

歸根結(jié)底還是因?yàn)殒i的粒度太大了。

有沒(méi)有更好的方式呢?

其實(shí)多個(gè)線程搶鎖,最后搶到鎖的線程才能進(jìn)行寫操作,從本質(zhì)上來(lái)看,就是將所有用戶發(fā)給GameServer邏輯服務(wù)器的消息給串行化了,

那既然是串行化,我完全可以在在業(yè)務(wù)代碼里為每個(gè)socket_fd配一個(gè)隊(duì)列來(lái)做,將數(shù)據(jù)在用戶態(tài)加鎖后塞到這個(gè)隊(duì)列里,再單獨(dú)開(kāi)一個(gè)線程,這個(gè)線程的工作就是發(fā)送消息給socket_fd。

于是上面的場(chǎng)景就變成了下面這樣。

圖片

并發(fā)寫到加鎖隊(duì)列后由一個(gè)線程處理

于是在gateway層,多個(gè)用戶線程同時(shí)寫消息時(shí),會(huì)去爭(zhēng)搶某個(gè)socket_fd對(duì)應(yīng)的隊(duì)列,搶到鎖之后就寫數(shù)據(jù)到隊(duì)列。而真正執(zhí)行 send(sockfd,msg) 的線程其實(shí)只有一個(gè)。它會(huì)從這個(gè)隊(duì)列中取數(shù)據(jù),然后不加鎖的批量發(fā)送數(shù)據(jù)到 GameServer。

由于加鎖后要做的事情很簡(jiǎn)單,也就塞個(gè)隊(duì)列而已,因此非???。并且由于執(zhí)行發(fā)送數(shù)據(jù)的只有單個(gè)線程,因此也不會(huì)有消息體亂序的問(wèn)題。

讀TCP Socket是線程安全的嗎?

在前面有了寫socket是線程安全的結(jié)論,我們稍微翻一下源碼就能發(fā)現(xiàn),讀socket其實(shí)也是加鎖了的,所以并發(fā)多線程讀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);

// ... 將數(shù)據(jù)從接收緩沖區(qū)拷貝到用戶緩沖區(qū)

// 釋放鎖
release_sock(sk);

}

但就算是線程安全,也不代表你可以用多個(gè)線程并發(fā)去讀。

因?yàn)檫@個(gè)鎖,只保證你在讀socket 接收緩沖區(qū)時(shí),只有一個(gè)線程在讀,但并不能保證你每次的時(shí)候,都能正好讀到完整消息體后才返回。

所以雖然并發(fā)讀不報(bào)錯(cuò),但每個(gè)線程拿到的消息肯定都不全,因?yàn)殒i的粒度并不保證能讀完完整消息。

TCP是基于數(shù)據(jù)流的協(xié)議,數(shù)據(jù)流會(huì)源源不斷從網(wǎng)卡那送到接收緩沖區(qū)。

如果此時(shí)接收緩沖區(qū)里有兩條完整消息,比如 "我是小白"和"點(diǎn)贊在看走一波"。

有兩個(gè)線程A和B同時(shí)并發(fā)去讀的話,A線程就可能讀到“我是 點(diǎn)贊走一波", B線程就可能讀到”小白 在看"

兩條消息都變得不完整了。

圖片

并發(fā)讀socket_fd導(dǎo)致的數(shù)據(jù)異常

解決方案還是跟讀的時(shí)候一樣,讀socket的只能有一個(gè)線程,讀到了消息之后塞到加鎖隊(duì)列中,再將消息分開(kāi)給到GameServer的多線程用戶邏輯模塊中去做處理。

圖片

單線程讀socket_fd后寫入加鎖隊(duì)列

讀寫UDP Socket是線程安全的嗎?

聊完TCP,我們很自然就能想到另外一個(gè)傳輸層協(xié)議UDP,那么它是線程安全的嗎?

我們平時(shí)寫代碼的時(shí)候如果要使用udp發(fā)送消息,一般會(huì)像下面這樣操作。

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

而執(zhí)行到底層,會(huì)到linux內(nèi)核的udp_sendmsg函數(shù)中。

int udp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg, size_t len) {
if (用到了MSG_MORE的功能) {
lock_sock(sk);
// 加入到發(fā)送緩沖區(qū)中
release_sock(sk);
} else {
// 不加鎖,直接發(fā)送消息
}
}

這里我用偽代碼改了下,大概的含義就是用到MSG_MORE就加鎖,否則不加鎖將傳入的msg作為一整個(gè)數(shù)據(jù)包直接發(fā)送。

首先需要搞清楚,MSG_MORE 是啥。它可以通過(guò)上面提到的sendto函數(shù)最右邊的flags字段進(jìn)行設(shè)置。大概的意思是告訴內(nèi)核,待會(huì)還有其他更多消息要一起發(fā),先別著急發(fā)出去。此時(shí)內(nèi)核就會(huì)把這份數(shù)據(jù)先用發(fā)送緩沖區(qū)緩存起來(lái),待會(huì)應(yīng)用層說(shuō)ok了,再一起發(fā)。

但是,我們一般也用不到 MSG_MORE。

所以我們直接關(guān)注另外一個(gè)分支,也就是不加鎖直接發(fā)消息。

那是不是說(shuō)明走了不加鎖的分支時(shí),udp發(fā)消息并不是線程安全的?

其實(shí)。還是線程安全的,不用lock_sock(sk)加鎖,單純是因?yàn)闆](méi)必要。

開(kāi)啟MSG_MORE時(shí)多個(gè)線程會(huì)同時(shí)寫到同一個(gè)socket_fd對(duì)應(yīng)的發(fā)送緩沖區(qū)中,然后再統(tǒng)一一起發(fā)送到IP層,因此需要有個(gè)鎖防止出現(xiàn)多個(gè)線程將對(duì)方寫的數(shù)據(jù)給覆蓋掉的問(wèn)題。而不開(kāi)啟MSG_MORE時(shí),數(shù)據(jù)則會(huì)直接發(fā)送給IP層,就沒(méi)有了上面的煩惱。

再看下udp的接收函數(shù)udp_recvmsg,會(huì)發(fā)現(xiàn)情況也類似,這里就不再贅述。

能否多線程同時(shí)并發(fā)讀或?qū)懲粋€(gè)UDP socket?

在TCP中,線程安全不代表你可以并發(fā)地讀寫同一個(gè)socket_fd,因?yàn)槟呐聝?nèi)核態(tài)中加了lock_sock(sk),這個(gè)鎖的粒度并不覆蓋整個(gè)完整消息的多次分批發(fā)送,它只保證單次發(fā)送的線程安全,所以建議只用一個(gè)線程去讀寫一個(gè)socket_fd。

那么問(wèn)題又來(lái)了,那UDP呢?會(huì)有一樣的問(wèn)題嗎?

我們跟TCP對(duì)比下,大家就知道了。

TCP不能用多線程同時(shí)讀和同時(shí)寫,是因?yàn)樗腔跀?shù)據(jù)流的協(xié)議。

那UDP呢?它是基于數(shù)據(jù)報(bào)的協(xié)議。

圖片

UDP是什么

基于數(shù)據(jù)流和基于數(shù)據(jù)報(bào)有什么區(qū)別呢?

基于數(shù)據(jù)流,意味著發(fā)給內(nèi)核底層的數(shù)據(jù)就跟水進(jìn)入水管一樣,內(nèi)核根本不知道什么時(shí)候是個(gè)頭,沒(méi)有明確的邊界。

而基于數(shù)據(jù)報(bào),可以類比為一件件快遞進(jìn)入傳送管道一樣,內(nèi)核很清楚拿到的是幾件快遞,快遞和快遞之間邊界分明。

圖片

水滴和快遞的差異

那從我們使用的方式來(lái)看,應(yīng)用層通過(guò)TCP去發(fā)數(shù)據(jù),TCP就先把它放到緩沖區(qū)中,然后就返回。至于什么時(shí)候發(fā)數(shù)據(jù),發(fā)多少數(shù)據(jù),發(fā)的數(shù)據(jù)是剛剛應(yīng)用層傳進(jìn)去的一半還是全部都是不確定的,全看內(nèi)核的心情。在接收端收的時(shí)候也一樣。

但UDP就不同,UDP 對(duì)應(yīng)用層交下來(lái)的報(bào)文,既不合并,也不拆分,而是保留這些報(bào)文的邊界。

無(wú)論應(yīng)用層交給 UDP 多長(zhǎng)的報(bào)文,UDP 都照樣發(fā)送,即一次發(fā)送一個(gè)報(bào)文。至于數(shù)據(jù)包太長(zhǎng),需要分片,那也是IP層的事情,跟UDP沒(méi)啥關(guān)系,大不了效率低一些。而接收方在接收數(shù)據(jù)報(bào)的時(shí)候,一次取一個(gè)完整的包,不存在TCP常見(jiàn)的半包和粘包問(wèn)題。

正因?yàn)榛跀?shù)據(jù)報(bào)和基于字節(jié)流的差異,TCP 發(fā)送端發(fā) 10 次字節(jié)流數(shù)據(jù),接收端可以分 100 次去取數(shù)據(jù),每次取數(shù)據(jù)的長(zhǎng)度可以根據(jù)處理能力作調(diào)整;但 UDP 發(fā)送端發(fā)了 10 次數(shù)據(jù)報(bào),那接收端就要在 10 次收完,且發(fā)了多少次,就取多少次,確保每次都是一個(gè)完整的數(shù)據(jù)報(bào)。

所以從這個(gè)角度來(lái)說(shuō),UDP寫數(shù)據(jù)報(bào)的行為是"原子"的,不存在發(fā)一半包或收一半包的問(wèn)題,要么整個(gè)包成功,要么整個(gè)包失敗。因此多個(gè)線程同時(shí)讀寫,也就不會(huì)有TCP的問(wèn)題。

所以,可以多個(gè)線程同時(shí)讀寫同一個(gè)udp socket。

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

為什么不建議使用多線程同時(shí)讀寫同一個(gè)UDP socket

udp本身是不可靠的協(xié)議,多線程高并發(fā)執(zhí)行發(fā)送時(shí),會(huì)對(duì)系統(tǒng)造成較大壓力,這時(shí)候丟包是常見(jiàn)的事情。雖然這時(shí)候應(yīng)用層能實(shí)現(xiàn)重傳邏輯,但重傳這件事畢竟是越少越好。因此通常還會(huì)希望能有個(gè)應(yīng)用層流量控制的功能,如果是單線程讀寫的話,就可以在同一個(gè)地方對(duì)流量實(shí)現(xiàn)調(diào)控。類似的,實(shí)現(xiàn)其他插件功能也會(huì)更加方便,比如給某些vip等級(jí)的老板更快速的游戲體驗(yàn)啥的(我瞎說(shuō)的)。

所以正確的做法,還是跟TCP一樣,不管外面有多少個(gè)線程,還是并發(fā)加鎖寫到一個(gè)隊(duì)列里,然后起一個(gè)單獨(dú)的線程去做發(fā)送操作。

圖片

udp并發(fā)寫加鎖隊(duì)列后再寫socket_fd

總結(jié)

1. 多線程并發(fā)讀/寫同一個(gè)TCP socket是線程安全的,因?yàn)門CP socket的讀/寫操作都上鎖了。雖然線程安全,但依然不建議你這么做,因?yàn)門CP本身是基于數(shù)據(jù)流的協(xié)議,一份完整的消息數(shù)據(jù)可能會(huì)分開(kāi)多次去寫/讀,內(nèi)核的鎖只保證單次讀/寫socket是線程安全,鎖的粒度并不覆蓋整個(gè)完整消息。因此建議用一個(gè)線程去讀/寫TCP socket。

2. 多線程并發(fā)讀/寫同一個(gè)UDP socket也是線程安全的,因?yàn)閁DP socket的讀/寫操作也都上鎖了。UDP寫數(shù)據(jù)報(bào)的行為是"原子"的,不存在發(fā)一半包或收一半包的問(wèn)題,要么整個(gè)包成功,要么整個(gè)包失敗。因此多個(gè)線程同時(shí)讀寫,也就不會(huì)有TCP的問(wèn)題。雖然如此,但還是建議用一個(gè)線程去讀/寫UDP socket。

最后

上面文章里提到,建議用單線程的方式去讀/寫socket,但每個(gè)socket都配一個(gè)線程這件事情,顯然有些奢侈,比如線程切換的代價(jià)也不小,那這種情況有什么好的解決辦法嗎?

責(zé)任編輯:武曉燕 來(lái)源: 小白debug
相關(guān)推薦

2021-06-08 11:15:10

Redis數(shù)據(jù)庫(kù)命令

2023-05-15 08:01:16

Go語(yǔ)言

2024-12-31 11:40:05

2012-06-13 16:37:50

2014-06-10 09:19:01

2022-07-01 16:02:36

開(kāi)源安全

2022-02-17 15:52:08

區(qū)塊鏈安全技術(shù)

2022-03-22 09:16:24

HTTPS數(shù)據(jù)安全網(wǎng)絡(luò)協(xié)議

2014-11-11 10:39:13

2021-02-14 00:45:08

區(qū)塊鏈加密貨幣安全令牌

2024-11-26 17:43:51

2023-07-28 08:04:56

StringHeaatomic線程

2020-09-03 06:42:12

線程安全CPU

2020-10-26 07:07:50

線程安全框架

2016-09-29 15:43:33

2011-03-01 09:07:40

2021-08-09 08:40:33

零知識(shí)證明零信任網(wǎng)絡(luò)安全

2018-12-12 08:02:56

物聯(lián)網(wǎng)物聯(lián)網(wǎng)安全IOT

2024-02-26 15:58:25

2022-02-16 14:31:47

區(qū)塊鏈網(wǎng)絡(luò)安全風(fēng)險(xiǎn)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)