本文轉載自【微信公眾號:小碼逆襲,ID:gh_7c5a039380a0】經微信公眾號授權轉載,如需轉載與原文作者聯繫
本篇文章你能學到:
1、實現簡單的c++版本的伺服器和客戶端
2、深入理解bind()函數,bind的系統調用過程,bind是如何處理埠衝突的,面試的時候你就這樣講。
注意:本片文章涉及到的內核源碼來自linux內核版本3.6
簡單的伺服器與客戶端實現
本篇文章的重點在於從底層深入分析bind()函數,相信已經能夠自己實現一個簡單的伺服器和客戶端並進行交互,下面是一個簡單的demo,幫助大家複習一下socket編程api的調用過程。
伺服器server.cpp
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main(){
//創建套接字
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//將套接字和IP、埠綁定
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr)); //每個字節都用0填充
serv_addr.sin_family = AF_INET; //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具體的IP位址
serv_addr.sin_port = htons(1234); //埠
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
//進入監聽狀態,等待用戶發起請求
listen(serv_sock, 20);
//接收客戶端請求
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_size = sizeof(clnt_addr);
int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
//向客戶端發送數據
char str[] = "http://c.biancheng.net/socket/";
write(clnt_sock, str, sizeof(str));
//關閉套接字
close(clnt_sock);
close(serv_sock);
return 0;
}
客戶端client.cpp
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
int main(){
//創建套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);
//向伺服器(特定的IP和埠)發起請求
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr)); //每個字節都用0填充
serv_addr.sin_family = AF_INET; //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具體的IP位址
serv_addr.sin_port = htons(1234); //埠
connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
//讀取伺服器傳回的數據
char buffer[40];
read(sock, buffer, sizeof(buffer)-1);
printf("Message form server: %s\n", buffer);
//關閉套接字
close(sock);
return 0;
}
socket編程TCP協議的調用流程如下:
深入分析bind函數
我從應用層出發,沿著網絡協議棧,從bind()的系統調用、到Socket層實現,最終到它的TCP層實現。
應用層
int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);
給socket描述符綁定IP和埠,一般伺服器才需要。
也可交給系統來選擇:
my_addr.sin_port = 0; 系統隨機選擇一個未被使用的埠
my_addr.sin_addr.s_addr = INADDR_ANY; 自動填入本機的IP位址
#define INADDR_ANY ((unsigned long int) 0x00000000)
埠號的範圍為0 ~ 65535。調用bind()時,一般不要把埠號置為小於1024的值,因為1到1023是保留埠號。
系統調用
bind()是由glibc提供的,聲明位於include/sys/socket.h中,實現位於sysdeps/mach/hurd/bind.c中,主要是用來從用戶空間進入名為sys_socketcall的系統調用,並傳遞參數。sys_scoketcall()實際上是所有socket函數進入內核空間的共同入口。
在sys_socketcall()中會調用sys_bind()。
SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
{
...
switch(call) {
...
case SYS_BIND:
err = sys_bind(a0, (struct sockaddr __user *)a1, a[2]);
break;
...
}
return err;
}
經過了socket層的總入口sys_socketcall(),現在進入sys_bind()。
/*
* Bind a name to a socket. Nothing much to do here since it's the protocol's responsibility
* to handle the local address.
* We move the socket address to kernel space before we call the protocol layer (having also
* checked the address is ok).
*/
SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen)
{
struct socket *sock;
struct sockaddr_storage address;
int err, fput_needed;
/* 通過文件描述符fd,找到對應的socket。
* 以fd為索引從當前進程的文件描述符表files_struct中找到對應的file實例,
* 然後從file實例的private_data成員中獲取socket實例。
*/
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (sock) {
/* 把用戶空間的地址複製到內核空間,成功返回0 */
err = move_addr_to_kernel(umyaddr, addrlen, &address);
if (err >= 0) {
/* SELInux相關 */
err = security_socket_bind(sock, (struct sockaddr *)&address, addrlen);
if (!err)
/* socket層的操作函數集。如果是SOCK_STREAM的話,proto_ops是inet_stream_ops,
* 接下來調用的是inet_bind()。
*/
err = sock->ops->bind(sock, (struct sockaddr *)&address, addrlen);
}
fput_light(sock->file, fput_needed);
}
return err;
}
通過文件描述符,找到對應的file結構。
static struct socket *sockfd_lookup_light(int fd, int *err, int *fput_needed)
{
struct file *file;
struct socket *sock;
*err = -EBADF; /* Bad file number */
/* 從當前進程的files_struct中找到網絡文件系統中的file指針,並增加它的引用計數 */
file = fget_light(fd, fput_needed);
if (file) {
sock = sock_from_file(file, err); /* 通過file找到對應的socket */
if (sock)
return sock;
fput_light(file, *fput_needed); /* 失敗的話減少file的引用計數 */
}
return NULL;
}
通過file結構,找到對應的socket結構。
struct socket *sock_from_file(struct file *file, int *err)
{
if (file->f_op == &socket_file_ops) /* 說明此file對應一個socket */
return file->private_data; /* set in sock_map_fd */
*err = -ENOTSOCK;
return NULL;
}
把用戶空間的socket地址複製到內核空間,同時檢查是否合法,成功返回0。
int move_addr_to_kernel(void __user *uaddr, int ulen, struct sockaddr_storage *kaddr)
{
if (ulen < 0 || ulen > sizeof(struct sockaddr_storage)) /* socket地址長度是否合法 */
return -EINVAL;
if (ulen == 0)
return 0;
if (copy_from_user(kaddr, uaddr, ulen))
return -EFAULT; /* socket地址是否合法 */
return audit_sockaddr(ulen, kaddr);
}
Socket層
SOCK_STREAM套接口的socket層操作函數集實例為inet_stream_ops,其中綁定函數為inet_bind()。
const struct proto_ops inet_stream_ops = {
.family = PF_INET,
.owner = THIS_MODULE,
...
.bind = inet_bind, /* socket層的bind實現 */
...
}
socket層做的主要事情為合法性檢查、綁定IP位址,而真正的埠綁定是在TCP層進行的。
int inet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len)
{
struct sockaddr_in *addr = (struct sockaddr_in *)uaddr;
struct sock *sk = sock->sk; /* 傳輸層實例 */
struct inet_sock *inet = inet_sk(sk); /* INET實例 */
unsigned short snum; /* 要綁定的埠 */
int chk_addr_ret; /* IP位址類型 */
int err;
/* If the socket has its own bind function then use it. (RAW)
* 用於原始套接字,TCP協議實例tcp_prot不含此函數指針。
*/
if (sk->sk_prot->bind) {
err = sk->sk_prot->bind(sk, uaddr, addr_len);
goto out;
}
err = -EINVAL;
if (addr_len < sizeof(struct sockaddr_in)) /* socket地址長度錯誤 */
goto out;
if (addr->sin_family != AF_INET) { /* 非INET協議族 */
/* Compatibility games: accept AF_UNSPEC (mapped to AF_INET)
* only if s_addr is INADDR_ANY.
*/
err = -EAFNOSUPPORT;
if (addr->sin_family != AF_UNSPEC || addr->sin_addr.s_addr != htonl(INADDR_ANY))
goto out;
}
/* 在路由中檢查IP位址類型,單播、多播還是廣播 */
chk_addr_ret = inet_addr_type(sock_net(sk), addr->sin_addr.s_addr);
/* Not specified by any standard per-se, however it breaks too many applications
* when removed. It is unfortunate since allowing applications to make a non-local
* bind solves several problems with systems using dynamic addressing.
* (ie. your servers still start up even if your ISDN link is temporarily down)
*/
/* sysctl_ip_nonlocal_bind表示是否允許綁定非本地的IP位址。
* inet->freebind表示是否允許綁定非主機地址。
* 這裡需要允許綁定非本地地址,除非是發送給自己、多播或廣播。
*/
err = -EADDRNOTAVAIL; /* Cannot assign requested address */
if (! sysctl_ip_nonlocal_bind && ! (inet->freebind || inet->transparent) &&
addr->sin_addr.s_addr != htonl(INADDR_ANY) &&
chk_addr_ret != RTN_LOCAL && chk_addr_ret != RTN_MULTICAST &&
chk_addr_ret != RTN_BROADCAST)
goto out;
snum = ntohs(addr->sin_port); /* 要綁定的埠 */
err = -EACCES; /* Permission denied */
/* snum為0表示讓系統隨機選擇一個未使用的埠,因此是合法的。
* 如要需要綁定的埠為1 ~ 1023,則需要對應的特權。
*/
if (snum && snum < PORT_SOCK && ! capable(CAP_NET_BIND_SERVICE))
goto out;
lock_sock(sk);
/* Check these errors (active socket, double bind).
* 如果套接字不在初始狀態TCP_CLOSE,或者已經綁定埠了,則出錯。
* 一個socket最多可以綁定一個埠,而一個埠則可能被多個socket共用。
*/
err = -EINVAL;
if (sk->sk_state != TCP_CLOSE || inet->inet_num)
goto out_release_sock;
/* We keep a pair of addresses. rcv_saddr is the one used by hash lookups,
* and saddr is used for transmit.
* In the BSD API these are the same except where it would be illegal to use them
* (multicast/broadcast) in which case the sending device address is used.
*/
inet->inet_rcv_saddr = inet->inet_saddr = addr->sin_addr.s_addr; /* 綁定地址 */
if (chk_addr_ret == RTN_MULTICAST || chk_addr_ret == RTN_BROADCAST)
inet->inet_saddr = 0; /* Use device */
/* Make sure we are allowed to bind here.
* 如果使用的是TCP,則sk_prot為tcp_prot,get_port為inet_csk_get_port()
* 埠可用的話返回0。
*/
if (sk->sk_prot->get_port(sk, snum)) {
inet->inet_saddr = inet->inet_rcv_saddr = 0;
err = -EADDRINUSE;
goto out_release_sock;
}
/* inet_rcv_saddr表示綁定的地址,接收數據時用於查找socket */
if (inet->inet_rcv_saddr)
sk->sk_userlocks |= SOCK_BINDADDR_LOCK; /* 表示綁定了本地地址 */
if (snum)
sk->sk_userlocks |= SOCK_BINDPORT_LOCK; /* 表示綁定了本地埠 */
inet->inet_sport = htons(inet->inet_num); /* 綁定埠 */
inet->inet_daddr = 0;
inet->inet_dport = 0;
sk_dst_reset(sk);
err = 0;
out_release_sock:
release_sock(sk);
out:
return err;
}
/* Sockets 0 - 1023 can't be bound to unless you are superuser */
#define PORT_SOCK 1024
/* Allows binding to TCP/UDP sockets below 1024 */
#define CAP_NET_BIND_SERVICE 10
TCP層
SOCK_STREAM套接口的TCP層操作函數集實例為tcp_prot,其中埠綁定函數為inet_csk_get_port()。
struct proto tcp_prot = {
.name = "TCP",
.owner = THIS_MODULE,
...
.get_port = inet_csk_get_port, /* TCP層bind()相關操作 */
...
};
和較早的內核版本不同,現在系統自動選擇埠時,也可以復用埠了。
/* Obtain a reference to a local port for the given sock,
* if snum is zero it means select any available local port.
*/
int inet_csk_get_port(struct sock *sk, unsigned short snum)
{
struct inet_hashinfo *hashinfo = sk->sk_prot->h.hashinfo; /* 指向tcp_hashinfo */
struct inet_bind_hashbucket *head;
struct hlist_node *node;
struct inet_bind_bucket *tb;
int ret, attempts = 5;
struct net *net = sock_net(sk);
int smallest_size = -1, smallest_rover;
local_bh_disable(); /* 禁止下半部,防止和進程衝突 */
/* 如果snum為0,系統自動為sock選擇一個埠號 */
if (! snum) {
int remaining, rover, low, high;
again:
inet_get_local_port_range(&low, &high); /* 獲取埠號的取值範圍 */
remaining = (high - low) + 1; /* 取值範圍內埠號的個數 */
smallest_rover = rover = net_random() % remaining + low; /* 隨機選取範圍內的一個埠 */
smallest_size = -1;
do {
if (inet_is_reserved_local_port(rover)) /* 查看埠是否屬於保留的 */
goto next_nolock; /* rover加1,繼續 */
/* 根據埠號,確定所在的哈希桶 */
head = &hashinfo->bhash[inet_bhashfn(net, rover, hashinfo->bhash_size)];
spin_lock( &head->lock); /* 鎖住哈希桶 */
inet_bind_bucket_for_each(tb, node, &head->chain) /* 從頭遍歷哈希桶 */
/* 如果埠被使用了 */
if (eq(ib_net(tb), net) && tb->port == rover) {
if (tb->fastreuse > 0 && sk->sk_reuse && sk->sk_state != TCP_LISTEN &&
tb->num_owners < smallest_size || smallest_size == -1)) {
smallest_size = tb->num_owners; /* 記下這個埠使用者的個數 */
smallest_rover = rover; /* 記下這個埠 */
/* 如果系統綁定的埠已經很多了,那麼就判斷埠是否有綁定衝突*/
if (atomic_read(&hashinfo->bsockets) > (high - low) + 1 &&
! inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb, false)) {
snum = smallest_rover; /* 沒有衝突,使用此埠 */
goto tb_found;
}
}
/* 檢查是否有埠綁定衝突,該埠是否能重用 */
if (! inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb, false)) {
snum = rover;
goto tb_found;
}
goto next; /* 此埠不可重用,看下一個 */
}
break; /* 找到了沒被用的埠,退出 */
next:
spin_unlock(&head->lock);
next_nolock:
if (++rover > high)
rover = low;
} while(--remaining > 0);
/* Exhausted local port range during search? It is not possible for us to be holding
* one of the bind hash locks if this test triggers, because if 'remaining' drops to zero,
* we broke out of the do/while loop at the top level, not from the 'break' statement.
*/
ret = 1;
if (remaining <= 0) { /* 完全遍歷 */
if (smallest_size != -1) {
snum = smallest_rover;
goto have_snum;
}
goto fail;
}
/* OK, here is the one we will use.
* HEAD is non-NULL and we hold it's mutex.
*/
snum = rover; /* 自動選擇的可用埠 */
} else { /* 如果有指定要綁定的埠 */
have_snum:
head = &hashinfo->bhash[inet_bhashfn(net, snum, hashinfo->bhash_size)];
spin_lock(&head->lock);
inet_bind_bucket_for_each(tb, node, &head->chain)
if (net_eq(ib_net(tb), net) && tb->port == snum)
goto tb_found; /* 發現埠在用 */
}
tb = NULL;
goto tb_not_found;
tb_found:
/* 埠上有綁定sock時 */
if (! hlist_empty(&tb->owners)) {
/* 這是強制的綁定啊,不管埠是否會綁定衝突!*/
if (sk->sk_reuse == SK_FORCE_REUSE)
goto success;
if (tb->fastreuse > 0 && sk->sk_reuse && sk->sk_state != TCP_LISTEN &&
smallest_size == -1) { /* 指定埠的情況 */
goto success;
} else {
ret = 1;
if (inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb, true)) { /* 埠綁定衝突 */
/* 自動分配的埠綁定衝突了,再次嘗試,最多重試5次。
* 我覺得以下if不必要,因為自動選擇時goto tb_found之前都有檢測過了。
*/
if (sk->sk_reuse && sk->sk_state != TCP_LISTEN && smallest_size != -1
&& --attempts >= 0) {
spin_unlock(&head->lock);
goto again;
}
goto fail_unlock; /* 失敗 */
}
}
}
tb_not_found:
ret = 1;
/* 申請和初始化一個inet_bind_bucket結構 */
if (!tb && (tb = inet_bind_bucket_create(hashinfo->bind_bucket_cachep, net, head, snum)) == NULL)
goto fail_unlock;
if (hlist_empty(&tb->owners)) {
if (sk->sk_reuse && sk->sk_state != TCP_LISTEN)
tb->fastreuse = 1;
else
tb->fastreuse = 0;
} else if (tb->fastreuse && (! sk->sk_reuse || sk->sk_state == TCP_LISTEN))
tb->fastreuse = 0;
success:
/* 賦值icsk中的inet_bind_bucket */
if (! inet_csk(sk)->icsk_bind_hash)
inet_bind_hash(sk, tb, snum);
WARN_ON(inet_csk(sk)->icsk_bind_hash != tb);
ret = 0;
fail_unlock:
spin_unlock(&head->lock);
fail:
local_bh_enable();
return ret;
}
埠區間
我們可以指定系統自動分配埠號時,埠的區間:
/proc/sys/net/ipv4/ip_local_port_range,默認為:32768 61000
也可以指定要保留的埠區間:
/proc/sys/net/ipv4/ip_local_reserved_ports,默認為空。
/* This struct holds the first and last local port number.
* 用於系統自動分配的埠區間。
*/
struct local_ports sysctl_local_ports __read_mostly = {
.lock = __SEQLOCK_UNLOCKED(sysctl_local_ports.lock),
.range = {32768, 61000},
};
埠綁定衝突
看下面這段代碼的實現:
int inet_csk_bind_conflict(const struct sock *sk, const struct inet_bind_bucket *tb, bool relax)
{
struct sock *sk2;
struct hlist_node *node;
int reuse = sk->sk_reuse; /* SO_REUSEADDR,表示處於TIME_WAIT狀態的埠允許重用 */
/* Unlike other sk lookup places we do not check for sk_net here, since all the socks
* listed in tb->owners list belong to the same net - the one this bucket belongs to.
* 遍歷此埠上的sock。
*/
sk_for_each_bound(sk2, node, &tb->owners) {
/* 衝突的條件1:不是同一socket、綁定在相同的設備上 */
if (sk != sk2 && ! inet_v6_ipv6only(sk2) && (! sk->sk_bound_dev_if || ! sk2->sk_bound_dev_if
|| sk->sk_bound_dev_if == sk2->sk_bound_dev_if)) {
/* 衝突的條件2:綁定在相同的IP上
* 衝突的條件3(符合一個即滿足):
* 3.1 本socket不允許重用
* 3.2 鍊表中的socket不允許重用
* 3.3 鍊表中的socket處於監聽狀態
*/
if (! reuse || ! sk2->sk_reuse || sk2->sk_state == TCP_LISTEN) {
const __u32 sk2_rcv_saddr = sk_rcv_saddr(sk2); /* sk2的綁定IP */
if (! sk2_rcv_saddr || ! sk_rcv_saddr(sk) || sk2_rcv_saddr == sk_rcv_saddr(sk))
break; /* 衝突了 */
}
/* 覺得這段代碼有好多冗餘,可以精簡下:)
* 3.4 relax為false
*/
if (! relax && reuse && sk2->sk_reuse && sk2->sk_state != TCP_LISTEN) {
const __be32 sk2_rcv_saddr = sk_rcv_saddr(sk2);
if (! sk2_rcv_saddr || ! sk_rcv_saddr(sk) || sk2_rcv_saddr == sk_rcv_saddr(sk))
break;
}
}
}
return node != NULL;
}
什麼條件下會出現衝突呢?
同時符合以下條件才會衝突:
綁定的設備相同(不允許自動選擇設備)綁定的IP位址相同(不允許自動選擇IP)以下條件任意一個:要綁定的socket不允許重用已綁定的socket不允許重用已綁定的socket處於監聽狀態 relax參數為false
能夠重用埠號的情況
綁定的設備不同綁定的IP位址不同要綁定的socket允許重用,且已綁定的socket允許重用,且已綁定的socket不處於監聽狀態,relex參數為true。自動選擇埠的思路