Unix環境高級編程(APUE)中介紹了套接字socket的使用,本文從開發者使用過程角度簡單介紹了服務器開啓監聽、客戶端發起連接、子線程創建的一些過程以及Unix中套接字的地址格式等內容。
連接過程
創建套接字地址
- 套接字地址結構
struct sockaddr_in { sa_family_t sin_family; /* address family */ in_port_t sin_port; /* port number */ struct in_addr sin_addr; /* IPv4 address */ unsigned char sin_zero[8]; /* filler */ };
- 創建套接字地址
sockaddr_in
(IPv4),sockaddr_in6
(IPv6) - 初始化套接字地址,以IPv4爲例
sin_family
表示族,可選IPv4 -AF_INET
或 IPv6 -AF_INET6
sin_port
地址對於的端口,應大於1024,否則需要superuser權限sin_addr
網絡地址的二進制表示,通過預定義的值如本地環回INADDR_LOOPBACK
、任意INADDR_ANY
設置,或通過inet_aton
函數將點分十進制(127.0.0.1)格式轉換爲所需格式,由於網絡字節序與主機字節序可能不同,因此會再使用htonl
,htons
,ntohl
,ntohs
等函數進行轉換。sin_zero
Linux中定義的填充字節,爲0
- 使用套接字地址
bind
,connect
等
服務器開啓監聽
創建套接字地址及套接字,綁定,開啓監聽。其中開啓監聽在收到請求之前會阻塞(block),請求到來後會返回分配的新套接字描述符。
- 創建套接字地址,如上文所述
- 創建套接字
socket(int domain, int type, int protocol)
,返回套接字描述符, -1 on errordomain
指定域,如IPv4 -AF_INET
或 IPv6 -AF_INET6
type
指定連接類型,有如下四種SOCK_DGRAM
定長、無連接、不可靠報文,默認 UPDSOCK_RAW
IP數據報接口SOCK_SEQPACKET
定長、有序、可靠、面向連接的報文,需要AF_UNIX
domainSOCK_STREAM
定長、可靠、雙向、面向連接的數據流,默認 TCP
protocol
協議,0爲默認,其它可選IPPROTO_IP
,IPPROTO_IPv6
,IPPROTO_TCP
,IPPROTO_UDP
- 綁定套接字到指定地址
bind(int sockfd, const struct sockaddr *addr, socklen_t len)
,0 OK, -1 on errorsockfd
套接字描述符addr
套接字地址,需要將sockaddr_in
類型轉換爲此類型(轉換指針的類型)len
地址的長度
- 開啓監聽
listen(int sockfd, int backlog)
,0 OK, -1 on errorsockfd
如前所述backlog
等待隊列中的請求個數
代碼
#define SVR_PORT 2300
#define SVR_BACKLOG 10
// listen
void start_listen() {
// init socket addr
sockaddr_in svr_addr;
memset(&svr_addr, 0, sizeof(sockaddr_in));
svr_addr.sin_family = AF_INET, svr_addr.sin_port = htons(SVR_PORT), svr_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// init socket to listen
int sd_li = socket(AF_INET, SOCK_STREAM, 0);
if (sd_li == -1)
err_exit(-1, "socket: create socket failed.");
// bind socket to addr
int err = bind(sd_li, (sockaddr *)&svr_addr, sizeof(svr_addr));
if (err != 0)
err_exit(err, "bind: bind failed.");
// listen
err = listen(sd_li, SVR_BACKLOG);
if (err == -1)
err_exit(err, "listen: listen failed.");
printf("start listen on port %d\n", SVR_PORT);
// accept client request
while (1) {
sockaddr_in addr_peer;
socklen_t len_peer;
int sd_acc = accept(sd_li, (sockaddr *)&addr_peer, &len_peer);
if (sd_acc == -1)
err_exit(sd_acc, "accept: accept client error.");
printf("connected to client %xd.\n", addr_peer.sin_addr.s_addr);
// start a new thread and send hello back
pthread_t subthread;
err = pthread_create(&subthread, NULL, client_thread_fn, (void *)sd_acc);
if (err != 0)
err_exit(err, "pthread_create: create sub thread error.");
}
}
客戶端連接建立
- 創建套接字地址及套接字,如上文所述
- 連接
connect(int sockfd, const struct sockaddr *addr, socklen_t len)
, 0 if OK, -1 on erorsockfd
如前所述addr
套接字地址len
地址長度
- 關閉套接字
close
,該方法實際上是關閉了套接字(文件)描述符,因此需要包含unistd.h
頭文件
代碼
#define SVR_HOST_STR "127.0.0.1"
// connect to server, return socket descriptor if success
int connect() {
// init socket
sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET, addr.sin_port = htons(SVR_PORT);
inet_aton(SVR_HOST_STR, &addr.sin_addr);
// create socket
int sd = socket(AF_INET, SOCK_STREAM, 0);
if (sd == -1)
err_exit(sd, "socket: create socket failed.");
// connect
int ret_conn = connect(sd, (sockaddr *)&addr, sizeof(addr));
if (ret_conn != 0)
err_exit(ret_conn, "connect: connect failed.");
return sd;
}
套接字地址格式
由於不同的domain地址格式不同,因此Unix定義了統一的格式 sockaddr
用來內部使用,而開發者則針對不同的domain使用特定的格式,如 sockaddr_in
和 sockaddr_in6
,使用時需要將這些domain特定的格式轉換爲 sockaddr
格式。以下爲Linux下這些格式的定義:
struct sockaddr {
sa_family_t sa_family; /* address family */
char sa_data[]; /* variable-length address */
};
struct sockaddr_in {
sa_family_t sin_family; /* address family */
in_port_t sin_port; /* port number */
struct in_addr sin_addr; /* IPv4 address */
unsigned char sin_zero[8]; /* filler */
};
struct in_addr {
in_addr_t s_addr; /* IPv4 address */
};
上述 sockaddr_in
格式中的 in_addr
表示因特網地址(二級制或者說整數形式,不同於點分十進制 127.0.0.1)。當然,Unix提供了 inet_ntop
和 inet_pton
兩個函數,實現點分十進制與二進制之間的轉換。前者將二進制轉換爲點分十進制,後者將點分十進制轉換爲二進制。其中點分十進制爲 char *restrict str
字符串,size
指字符串緩衝區的長度;二進制爲 void *restrict addr
。
const char *inet_ntop(int domain, const void *restrict addr, char *restrict str, socklen_t size);
int inet_pton(int domain, const char *restrict str, void *restrict addr);
如前所述,由於網絡字節序Byte Order與主機字節序可能不同,因此Unix提供了下列四個函數用來轉換,其中h
表示主機host
,n
表示network
,l
和s
分別表示long
和short
:
uint32_t htonl(uint32_t hostint32);
Returns: 32-bit integer in network byte order
uint16_t htons(uint16_t hostint16);
Returns: 16-bit integer in network byte order
uint32_t ntohl(uint32_t netint32);
Returns: 32-bit integer in host byte order
uint16_t ntohs(uint16_t netint16);
Returns: 16-bit integer in host byte order
數據傳輸
數據傳輸過程主要爲發送和接收數據兩種,兩種各包含四個相似函數可以使用,這裏主要介紹其中第一種,即 send
和 recv
,如下:
ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags);
ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags);
前三個參數不難理解,分別是套接字描述符、待發送/接收消息緩衝區,緩衝區長度,最後一個是標誌。一般爲0即可,其它值如 MSG_OOB
, MSG_PEEK
等表示不同含義,此處不一一列出。
線程創建
線程創建主要使用 pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
函數,注意,g++需要在編譯時加上參數 -lpthread
才能使用相關函數。 attr
爲屬性,一般設置爲 NULL
即可,start_routine
函數爲子線程的開始函數,arg
爲其參數。子線程創建後會從該函數開始執行。
代碼
// thread function
void *thr_fn(void *x) {
//...
}
int main{
int sd = 1; // para
pthread_t sub_thr;
int err = pthread_create(&sub_thr, NULL, thr_fn, (void *)sd);
if (err != 0)
err_exit(err, "create sub thread failed.");
}