socket是程序通訊機制的一種,與PIPE、FIFO不同的是,socket即可以在同一臺主機通訊(unix domain),也可以通過網路在不同主機上的程序間通訊(如:ipv4、ipv6),例如因特網,應用層通過呼叫socket API來與核心TCP/IP協定棧的通訊,通過網路位元組實現不用主機之間的資料傳輸。
對於多位元組的資料,不同處理器儲存位元組的順序稱為位元組序,主要有大端序(big-endian)和小端序(little-endian),位元組序的收發不統一就會導致值被解析錯誤。
高位位元組存低位記憶體
大端序是最高位位元組儲存在最低位記憶體地址處。例如一段資料0x0A0B0C0D,0x0A是最高位位元組,0x0D是最地位位元組,記憶體地址最低位a、最高位a+3,在大端序中儲存方式如下
低位位元組存低位記憶體
小端序是最低位位元組儲存在最低位記憶體地址處。例如一段資料0x0A0B0C0D,0x0A是最高位位元組,0x0D是最地位位元組,記憶體地址最低位a、最高位a+3,在小端序儲存方式如下
主機通常使用小端序,因為計算機先處理小端序的位元組效率更高。通過上面的結構不難看出,大端序更易讀,所以網路和儲存等採用了大端序,那麼網路通訊的時候就需要將網路位元組的大端序轉換為主機位元組的小端序。好在這些都有系統呼叫可以保證~
判斷主機的位元組序:
#include <iostream>
using namespace std;
void byteorder() {
union {
short value;
char union_bytes[sizeof(short)];
} test;
test.value = 0x0102;
if ((test.union_bytes[0] == 0x01) && (test.union_bytes[1] == 0x02)) {
cout << "big endian" << endl; // [0x01, 0x02]
} else if ((test.union_bytes[0] == 0x02) && (test.union_bytes[1] == 0x01)) {
cout << "little endian" << endl; // [0x02, 0x01]
} else {
cout << "unknow~" << endl;
}
}
int main() { byteorder(); }
#include<netinet/in.h>
// long型主機位元組序轉換為long型網路位元組序, host to network
unsigned long int htonl(unsigned long int hostlong);
// short型
unsigned short int htons(unsigned short int hostshort);
// long型網路位元組序轉換為long型主機位元組序, network to host
unsigned long int ntohl(unsigned long int netlong);
// short型
unsigned short int ntohs(unsigned short int netshort);
比方轉換主機的埠
int main(int argc, char *argv[]){
int port = atoi(argv[1]); // 主機序
server_address.sin_port = htons(port); // 網路序
}
地址我們標識通訊的端點,通用的地址格式為
#include<bits/socket.h>
struct sockaddr
{
sa_family_t sa_family; // 協定型別,例如 ipv4 AF_INET、unix AF_UNIX
char sa_data[14]; // unix域存放檔案路徑,ip域存放ip地址和埠號
}
sa_data
只能容納14位元組地址資料,如果是unix域路徑長度可以達到108位元組放不下,所以linux定義了新的地址
#include<bits/socket.h>
struct sockaddr_storage
{
sa_family_t sa_family;
unsigned long int__ss_align; // 作用是記憶體對齊
char__ss_padding[128-sizeof(__ss_align)];
}
專有地址在bind、accept、connect等需要用到的函數中需要強制轉換為通用地址,例如:(struct sockaddr *)&server_address
顧名思義專門為ipv4、unix、ipv6設計的不同socket地址結構,以ipv4為例
struct sockaddr_in
{
sa_family_t sin_family; // AF_INET
u_int16_t sin_port; // 網路位元組序的埠號
struct in_addr sin_addr; // IP地址
};
struct in_addr
{
u_int32_t s_addr; // 網路位元組序的IP地址
};
具體這樣用:
int main(int argc, char *argv[]) {
const char *ip = argv[1]; // 主機序ip地址
int port = atoi(argv[2]); // 主機序埠
struct sockaddr_in address; // ipv4專有地址
// 設定專有地址的成員
address.sin_family = AF_INET;
address.sin_port = htons(port);
// 將點分10進位制的ip字串轉換為網路位元組序整形表示的ip地址,存入sin_addr
inet_pton(AF_INET, ip, &address.sin_addr);
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 建立socket
// 繫結埠,要強制轉換為通用地址 (struct sockaddr *)&address
int ret = bind(sockfd, (struct sockaddr *)&address, sizeof(address));
}
Linux一切皆檔案,所以socket建立好之後就是一個檔案描述符,對該fd讀寫關閉、屬性控制。
以ipv4為例
#include <sys/socket.h>
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
標識該socket,對於ipv4用ip地址和埠作為端點的表示
int ret = bind(sockfd, (struct sockaddr *)&address, sizeof(address));
成功返回0,失敗返回-1並設定errno,例如errno
SO_REUSEADDR
來複用處於TIME_WAIT連線的埠和地址開始監聽,並指定連線數
#include<sys/socket.h>
int listen(int sockfd,int backlog);
ret = listen(sock, 5);
從listen佇列中拿連線過來,不管該連線是ESTABLISED還是CLOSE_WAIT的狀態。
int connfd = accept(sockfd, (struct sockaddr *)&client, &client_addrlength);
connect(sockfd, (struct sockaddr *)&server_address, sizeof(server_address))
成功返回0,失敗返回-1並設定errno
關閉socket fd,預設情況下:如果是多程序,fork後會將fd參照計數加1,如果要關閉該socket,父子程序都需要close,而且是同時關閉讀和寫。可以通過setsockopt的SO_LINGER控制close的行為
#include<sys/socket.h>
struct linger
{
int l_onoff; // 關閉控制
int l_linger; // 控制時間
}
close可能會有三種行為:
#include<sys/socket.h>
int shutdown(int sockfd,int howto);
不參照計數直接關閉,howto引數:
除了預設對檔案描述符的read、write操作之外,socket提供了專門的讀寫資料函數
#include<sys/socket.h>
// recv成功時返回讀取到的長度,實際長度可能小於len
// 發生錯誤返回-1設定errno,返回0表示連線關閉
ssize_t recv(int sockfd, void*buf, size_t len, int flags);
// 成功時返回寫入的資料的長度,失敗返回-1這是errno
ssize_t send(int sockfd, const void*buf, size_t len, int flags);
flags提供了一些選項設定:
char buffer[1024];
memset(buffer, '\0', 1024);
// 傳送端傳送帶外資料hello
const char *oob_data = "hello";
send(sockfd, oob_data, strlen(oob_data), MSG_OOB);
ret = recv(connfd, buffer, BUFESIZE - 1, 0);
// 接收到hell
ret = recv(connfd, buffer, BUFESIZE - 1, MSG_OOB); // 接收端接收帶外資料
// 接收到o
hell為正常資料,o為帶外資料,只有最後一個位元組會被認為是帶外資料,前面的是正常資料。正常資料的接收會被帶外資料截斷。
int sockatmark(int sockfd);
可以判斷下一個資料是不是帶外資料,1為是,此時可以利用MSG_OOB標誌的recv呼叫來接收帶外資料。通常這兩個函數用於無連線的通訊端,如果用於有連線的讀寫可以把後兩位置為NULL
#include <sys/socket.h>
// 可以接收UDP,也可以接收TCP(後兩個引數置位NULL,因為TCP是面向連線的)
ssize_t recvfrom(int sockfd,void* buf,size_t len,int flags,
struct sockaddr* src_addr,socklen_t* addrlen);
// 可以接收UDP,也可以接收TCP(後兩個引數置位NULL,因為TCP是面向連線的)
ssize_t sendto(int sockfd,const void* buf,size_t len,int flags,
const struct sockaddr* dest_addr,socklen_t addrlen);
使用sendmsg可以將多個緩衝區的資料合併行送
使用recvmsg可以將接收的資料送入多個緩衝區,或者接收輔助資料
#include<sys/socket.h>
ssize_t recvmsg(int sockfd,struct msghdr* msg,int flags);
ssize_t sendmsg(int sockfd,struct msghdr* msg,int flags);
msghdr結構
struct msghdr
{
void* msg_name; // socket地址,如果是流資料,設定為NULL
socklen_t msg_namelen; // 地址長度
struct iovec* msg_iov; // I/O快取區陣列,分散的緩衝區
int msg_iovlen; // I/O快取區陣列元素數量
void* msg_control; // 輔助資料起始位置
socklen_t msg_controllen; // 輔助資料位元組數
int msg_flags; // 等於recvmsg和sendmsg的flags引數,在呼叫過程中更新
};
#include<sys/socket.h>
// 獲取socketfd本端的地址資訊,存到address,如果address長度大於address_len,將被截斷
int getsockname(int sockfd,struct sockaddr*address,socklen_t*address_len);
// 獲取socketfd遠端的地址資訊
int getpeername(int sockfd,struct sockaddr*address,socklen_t*address_len);
成功返回0,失敗返回-1設定errno
#include<sys/socket.h>
int getsockopt(int sockfd,int level,int option_name,
void*option_value,socklen_t*restrict option_len);
int setsockopt(int sockfd,int level,int option_name,
const void*option_value,socklen_t option_len);
成功返回0,失敗返回-1設定errno,記錄一下option_name,後面用到結合具體範例分析
根據主機名稱獲取主機的完整資訊、根據地址獲取主機的完整資訊,資訊返回結構如下:
#include<netdb.h>
struct hostent
{
char* h_name; /*主機名*/
char** h_aliases; /*主機別名列表,可能有多個*/
int h_addrtype; /*地址型別(地址族)*/
int h_length; /*地址長度*/
char** h_addr_list /*按網路位元組序列出的主機IP地址列表*/
};
根據服務名稱或埠號獲取服務資訊,從/etc/services
獲取資訊,該檔案中存放的是知名埠號和協定等資訊。返回結構體如下:
#include<netdb.h>
struct servent
{
char* s_name; /*服務名稱*/
char** s_aliases; /*服務的別名列表,可能有多個*/
int s_port; /*埠號*/
char* s_proto; /*服務型別,通常是tcp或者udp*/
};
可以認為是呼叫了gethostbyname和getservbyname
#include<netdb.h>
// hostname:可以是主機名或IP地址字串
// service:可以接收服務名,也可以接收十進位制埠號
// result指向返回結果的連結串列,結構為addrinfo
int getaddrinfo(const char* hostname,const char* service,const
struct addrinfo* hints,struct addrinfo** result);
addrinfo結構體:
struct addrinfo
{
int ai_flags; /*大部分設定hints引數*/
int ai_family; /*地址族*/
int ai_socktype; /*服務型別,SOCK_STREAM或SOCK_DGRAM*/
int ai_protocol; /*通常設定為0*/
socklen_t ai_addrlen; /*socket地址ai_addr的長度*/
char* ai_canonname; /*主機的別名*/
struct sockaddr* ai_addr; /*指向socket地址*/
struct addrinfo* ai_next; /*指向下一個sockinfo結構的物件*/
};
getaddrinfo結束後,釋放result分配的堆記憶體
void freeaddrinfo(struct addrinfo* res);
可以認為是呼叫了gethostbyaddr和getservbyport
#include<netdb.h>
// 返回的主機名儲存在host,服務名儲存在serv
int getnameinfo(const struct sockaddr *sockaddr,socklen_t addrlen,
char* host,socklen_t hostlen,char *serv,socklen_t servlen,int flags);
轉換getnameinfo和getaddrinfo返回的錯誤碼為可讀的字串
#include<netdb.h>
const char* gai_strerror(int error);
getaddrinfo和getnameinfo返回的錯誤碼如下:
testserver.cc,testserver 0.0.0.0 8889
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cassert>
#include <cstdio>
#include <iostream>
using namespace std;
int main(int argc, char *argv[]) {
if (argc <= 2) {
cout << "usage:" << argv[0] << " ip_address port_number" << endl;
return 0;
}
const char *ip = argv[1];
int port = atoi(argv[2]);
struct sockaddr_in address, client_addr;
address.sin_family = AF_INET;
address.sin_port = htons(port);
inet_pton(AF_INET, ip, &address.sin_addr);
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
int ret = bind(sockfd, (struct sockaddr *)&address, sizeof(address));
assert(ret != -1);
ret = listen(sockfd, 2);
assert(ret != -1);
socklen_t client_addr_length = sizeof(client_addr);
int conn =
accept(sockfd, (struct sockaddr *)&client_addr, &client_addr_length);
if (conn < 0)
cout << "connect error: " << errno << endl;
else {
string hello = "hello client";
send(conn, hello.data(), sizeof(hello), 0);
close(conn);
}
close(sockfd);
return 0;
}
testclient.cc,/etc/hosts加入server的地址和主機名,testclient myserver
#include <netdb.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cassert>
#include <iostream>
using namespace std;
int main(int argc, char* argv[]) {
if (argc != 2) {
cout << "usage: " << argv[0] << " hostname" << endl;
return 0;
}
char* hostname = argv[1];
// 獲取主機資訊
struct hostent* hostinfo = gethostbyname(hostname);
assert(hostinfo);
/*
獲取server返回資訊,自定義一個服務,
編輯/etc/services, my 8889/tcp
*/
struct servent* servinfo = getservbyname("my", "tcp");
assert(servinfo);
cout << "myserver port is " << ntohs(servinfo->s_port) << endl;
struct sockaddr_in address;
address.sin_family = AF_INET;
address.sin_port = servinfo->s_port;
address.sin_addr = *(struct in_addr*)*hostinfo->h_addr_list;
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
int result = connect(sockfd, (struct sockaddr*)&address, sizeof(address));
assert(result != -1);
char buffer[128];
result = recv(sockfd, buffer, sizeof(buffer), 0);
cout << "resceived: " << result << endl;
assert(result > 0);
buffer[result] = '\0';
cout << "server's message: " << buffer << endl;
close(sockfd);
return 0;
}
學習自:
《Linux高效能伺服器程式設計》
《UNIX環境高階程式設計》
《UNIX系統程式設計》