網路程式設計教程(四)Linux網路程式設計基礎API
首先介紹Linux下整個的網路程式設計流程:
一、socket地址API
1.主機位元組序和網路位元組序
位元組序分為大端位元組序(big endian)和小端位元組序(little endian)。大端位元組序是指一個整數的搞我位元組儲存在記憶體的低地址處,低位位元組儲存在記憶體的高地址處。小端位元組序則是整數的高位位元組儲存在記憶體的高地址處,而低位位元組則儲存在記憶體的低地址處。
一般,大端位元組序也稱為網路位元組序,小端位元組序也稱為主機位元組序。
#include <netinet/in.h> unsigned long int htonl(unsigned long int hostlog);//將長整型的主機位元組序資料轉為網路位元組序資料 unsigned short int htons(unsigned short int hostshort); // unsigned long int ntohl(unsigned long int netlong); unsigned short int ntohs(unsigned short int netshort);
2.通用socket地址
#include <bits/socket.h>
struct sockaddr
{
sa_family_t sa_family; //地址族
char sa_data[14];
};
3.專用socket地址
上面這個通用socket地址結構體顯然很不好用,比如設定與獲取IP地址和埠號就需要執行繁瑣的位操作。TCP/IP協議族有sockaddr_in和sockaddr_in6兩個專用socket地址結構體,他們分別用於IPv4和IPv6,這裡只介紹常用的IPv4socket地址:
struct sockaddr_in
{
sa_family_t sin_family; //地址族,AF_INET
u_int16_t sin_port; //埠號
struct in_addr sin_addr; //IPv4結構體
};
struct in_addr
{
u_int32_t s_addr; //IPv4地址,要用網路位元組序表示
};
所有專用socket地址型別的變數在實際使用時都需要轉化為通用socket地址型別sockaddr(強制型別轉換),因為所有socket編成介面使用的地址引數型別都是sockaddr。
4.IP地址轉換函式
#include <arpa/inet.h>
//將用點分十進位制字串表示的IPv4地址轉化為網路位元組序整數表示的IPv4地址,失敗時返回INADDR_NONE.
in_addr_t inet_addr(const char* strptr);
//將點分十進位制字串表示的IPv4地址轉化為網路位元組序表示的IPv4地址,存放於inp中
int inet_aton(const char *cp, struct in_addr *inp);
//將用網路位元組序整數表示的IPv4地址轉化為用點分十進位制字串表示的IPv4地址
char* inet_ntoa(struct in_addr in);
//將字串表示的IP地址src轉為網路位元組序整數表示的IP地址,並存放在dst中,其中af指定地址族
int inet_pton(int af, const char* src, void *dst);
//與上面相反
const char* inet_ntop(int af, const void* src, char* dst, socklen_t cnt);
二、socket基礎API
1.建立socket
socket是一個可讀、可寫、可控制和可關閉的檔案描述符。下面的socket系統呼叫建立一個socket:
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
引數:
domain :指定協議族,一般用PF_INET
type :指定服務型別,主要有SOCK_STREAM服務(流服務)和SOCK_UGRAM(資料報)服務
protocol:一般設定為0,表示使用預設協議。因為前兩個引數已經唯一確定了是使用TCP還是UDP協議
返回值:
socket系統呼叫成功時返回一個socket檔案描述符,失敗則返回-1並設定errno.
2.命名socket
建立socket時,指定了地址族,但是沒有指定具體使用哪個socket地址,將一個socket與socket地址繫結稱為給socket命名。在伺服器程式中要命名socket,因為只有命名後客戶端才能知道該如何連線它。客戶端通常不需要命名socket,而是採用匿名方式,即使用作業系統自動分配的socket地址。
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen);
功能:
將my_addr所指的socket地址分配給未命名的sockfd
引數:
sockfd :呼叫socket()建立的socket檔案描述符
my_addr:socket地址
addrlen:my_addr的長度
返回值:
bind成功時返回0,失敗則返回-1,並設定errno.其中兩種常見的errno是EACCES和EADDRINUSE,EACCES是指被繫結的地址是受保護的
地址,而EADDRINUSE是指被繫結的地址正在使用中。
3.監聽socket
socket被命名以後,還不能馬上接受客戶連線,需要使用如下系統呼叫建立一個監聽佇列以存放待處理的客戶連線:
#include <sys/socket.h>
int listen(int sockfd, int backlog);
功能:
建立一個監聽佇列用於存放待處理的連線
引數:
sockfd :指定被監聽的socket
backlog:指示核心監聽佇列的最大長度
返回值:
listen成功時返回0,失敗則返回-1並設定errno。
4.接收連線
每當連線到來時就會被放入listen()系統呼叫建立的監聽佇列中,這時需要呼叫accept()從監聽佇列中接受一個連線:
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能:
從listen監聽佇列中接受一個連線
引數:
sockfd :執行過listen系統呼叫的監聽socket。
addr :用於獲取被接受連線的源端socket地址
addrlen:指定addr的長度
返回值:
成功時返回一個新的連線socket,該socket唯一地標識了被接受的這個連線,伺服器可以通過讀寫該socket來與被接受連線的客戶端通訊。失敗時返回-1並設定errno.
5.發起連線
伺服器通過listen呼叫來被動接受連線,那麼客戶端需要通過connect()系統呼叫來與伺服器建立連線:
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *servAddr, socklen_t addrlen);
功能:
與伺服器建立連線
引數:
sockfd :由socket()系統呼叫返回
servAddr:伺服器監聽的socket地址
addrlen :指定servAddr的長度
返回值:
connect成功時返回0.一旦成功建立連線,sockfd就唯一標識了這個連線,客戶端就可以通過讀寫該sockfd來與伺服器通訊。connect失敗時返回-1並設定errno,兩種常見的errno是ECONNREFUSED和ETIMEOUT,ECONNREFUSED表示目標埠不存在,ETIMEOUT表示連線超時。
6.關閉連線
關閉一個連線實際上是關閉該連線對應的socket。
#include <unistd.h>
int close(int fd);
功能:
關閉檔案描述符。close系統呼叫並非總是關閉一個連線,而是將fd引用計數減1,只有當fd引用計數為0時,才真正關閉連線。
引數:
fd:指定要關閉的檔案描述符
返回值:
成功時返回0,失敗時返回-1並設定errno.
三、資料讀寫
1.TCP資料讀寫
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
功能:
從sockfd中讀取len位元組的資料到buf中
引數:
sockfd:建立連線後的檔案描述符
buf :讀緩衝區的位置
len :讀緩衝區的大小
flags :一般設定為0
返回值:
recv成功時返回實際讀取到的資料的長度,它可能返回0,這意味著通訊對方已經關閉連線,出錯時返回-1並設定errno.
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
功能:
將buf中len位元組資料寫入sockfd中
引數:
sockfd:建立連線後的檔案描述符
buf :寫緩衝區的位置
len :寫緩衝區的大小
flags :一般設定為0
返回值:
send成功時返回實際寫入資料的長度,失敗時則返回-1並設定errno.
2.UDP資料讀寫
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *srcaddr, socklen_t *addrlen);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
struct sockaddr *destaddr, socklen_t addrlen);
3.通用資料讀寫函式
#include <sys/socket.h>
ssize_t recvmsg(int sockfd, struct msghdr* msg, int flags);
ssize_t sendmsg(int sockfd, struct msghdr* msg, int flags);
4.socket選項
socket選項 | 作用 |
SO_REUSEADDR | 強制使用被處於TIME_WAIT狀態的連線佔用的socket地址 |
SO_RCVBUF | 設定接收緩衝區大小 |
SO_SNDBUF | 設定傳送緩衝區大小 |
SO_RCVLOWAT | 接收緩衝區的低水位標記 |
SO_SNDLOWAT | 傳送緩衝區的低水位標記 |
SO_LINGER | 控制close()系統呼叫在關閉TCP連線時的行為 |
四、例項程式碼分析
客戶端程式碼:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main( int argc, char* argv[] )
{
if( argc <= 2 )
{
printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
return 1;
}
const char* ip = argv[1];
int port = atoi( argv[2] );
struct sockaddr_in server_address;
bzero( &server_address, sizeof( server_address ) );
server_address.sin_family = AF_INET;
inet_pton( AF_INET, ip, &server_address.sin_addr );
server_address.sin_port = htons( port );
int sockfd = socket( PF_INET, SOCK_STREAM, 0 ); //建立socket,使用TCP協議
assert( sockfd >= 0 );
//發起連線
int ret = connect( sockfd, (struct sockaddr*)&server_address, sizeof(server_address));
if(ECONNREFUSED == ret)
{
printf("目標埠不存在,連線被拒絕.\n");
}
else if(ETIMEOUT == ret)
{
printf("連線超時.\n");
}
else if(ret < 0)
{
printf("connetion failed.\n");
}
else
{
printf( "send oob data out\n" );
const char* normal_data = "123";
send( sockfd, normal_data, strlen( normal_data ), 0 ); //傳送資料
}
close( sockfd ); //關閉連線
return 0;
}
服務端程式碼:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
const int BUF_SIZE = 1024;
int main( int argc, char* argv[] )
{
if( argc <= 2 )
{
printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
return 1;
}
const char* ip = argv[1];
int port = atoi( argv[2] );
struct sockaddr_in address;
bzero( &address, sizeof( address ) );
address.sin_family = AF_INET;
inet_pton( AF_INET, ip, &address.sin_addr );
address.sin_port = htons( port );
int sock = socket( PF_INET, SOCK_STREAM, 0 ); //建立socket,使用TCP協議
assert( sock >= 0 );
//命名socket
int ret = bind( sock, ( struct sockaddr* )&address, sizeof( address ) );
assert( ret != -1 );
//監聽socket
ret = listen( sock, 5 );
assert( ret != -1 );
struct sockaddr_in client;
socklen_t client_addrlength = sizeof( client );
//接收連線
int connfd = accept( sock, ( struct sockaddr* )&client, &client_addrlength );
if ( connfd < 0 )
{
printf( "errno is: %d\n", errno );
}
else
{
char buffer[ BUF_SIZE ];
memset( buffer, '\0', BUF_SIZE );
ret = recv( connfd, buffer, BUF_SIZE-1, 0 );
printf( "got %d bytes of normal data '%s'\n", ret, buffer );
close( connfd ); //關閉連線socket
}
close( sock ); //關閉監聽socket
return 0;
}