socket通訊:客戶端和伺服器的簡單實現
什麼是socket?
socket最開始的含義是一個地址和埠對(ip, port)。Socket又稱"套接字",應用程式通常通過"套接字"向網路發出請求或者應答網路請求。
- socket地址API:它唯一的表示了使用tcp通訊的一端,也可以將其理解成socket地址。
- socket基礎API:socket的主要API都定義在sys/socket.h標頭檔案中,包括建立socket,命名socket,監聽socket,接收連線,發起連線,讀寫資料等等。
主機位元組序和網路位元組序
現代cpu的累加器一次都能裝載4位元組,即一個整數。那麼這四個位元組在記憶體中的排列順序就會影響它被累加器裝載成的整數的值
- 大端位元組序:一個整數的高位位元組儲存在記憶體中的低地址處,低位位元組儲存在記憶體中的高地址處。
- 小端位元組序:一個整數的高位位元組儲存在記憶體中的高地址處,低位位元組儲存在記憶體中的高地址處。
現代PC機大多數採用小端位元組序,因此小端位元組序又被稱為主機位元組序。當格式化的資料在兩臺使用不同位元組序的主機之間直接傳遞時(不經過位元組序的轉換統一),接收端必然錯誤的解釋之。因此人們提出了一種解決辦法:傳送端總是把要傳送的資料轉化成大端位元組序資料後再發送,而接收端就預設接收到的資料都是採用大端位元組序,再根據自身的位元組序決定是否對齊進行轉化。因此大端位元組序也被稱為網路位元組序
位元組序的轉換函式
Linux系統提供瞭如下4個函式來完成主機位元組序和網路位元組序的轉化。
#include<netinet/in.h> unsigned long int htonl(unsigned long int hostlong);//IP地址從主機位元組序轉化成網路位元組序 unsigned short int htons(unsigned short int hostport);//埠號從主機位元組序轉化為網路位元組序 unsigned long int ntohl(unsigned long int netlong);//IP地址從網路位元組序轉化為主機位元組序 unsigned short int ntohs(unsigned short int netshort);//埠號從網路位元組序轉化為主機位元組序
位元組序和socket通訊之間的關係:我們知道socket套接字就是兩個主機之間通訊的方式,需要經過傳輸層和網路層協議(tcp/ip)來進行資料的傳輸,而這裡就會牽扯到位元組序的問題。需要進行位元組序的轉化才能將正確的資料傳出去和接收到。
專用socket地址
TCP/IP協議族有sockaddr_in這個用於IPV4的專用socket地址結構體
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。
IP地址轉化函式
人們通常會使用可讀性好的點分十進位制字串表示IPv4地址,但是在程式設計過程中我們要先將其轉化成整數才能使用。我們經常使用下面這個函式將點分十進位制字串表示的IPv4地址轉化為用網路位元組序整數表示的ipv4地址。
in_addr_t inet_addr(const *strptr); /*strptr表示點分十進位制字串形式的IPv4地址*/
一·建立socket:
linux/unix系統下以切皆檔案,socket也不例外,它就是一個可讀可寫,可控制,可關閉的檔案描述符。
#include<sys/types.h>
#include<sys/socket.h>
int socket(int domain, int type, int protocol); /*成功返回一個檔案描述符,失敗返回-1*/
- domain:該引數告訴系統使用哪個底層協議族,對於tcp/ip協議族而言,該引數應該設定為PF_INET(用於IPv4)或者PF_INET6(用於IP v6)。
- type:該引數指定服務型別,主要有SOCK_STREAM服務(流服務)和SOCK_UGRAM(資料報服務)。
- protocol:該引數是在前兩個引數構成的協議集合下,再選擇一個具體的協議。不過這個值通常都是唯一的。幾乎在所有情況下,我們都應該把它設定為0.表示使用預設協議。
二·命名socket
將一個socket與socket地址(上面的sockaddr_in結構體)繫結稱為給socket命名。在伺服器端我們通常要命名socket,只有命名之後客戶端才知道怎樣連線它。而客戶端通常不需要命名socket,而是採用匿名方式。下面這個系統呼叫專門用來命名socket:
#include<sys/types.h>
#include<sys/socket.h>
int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);
/成功返回0,失敗返回-1*/
- sockfd:檔案描述符
- my_addr:socket地址
- addrlen:socket地址長度
三·監聽socket
socket被命名之後,還不能馬上接收客戶連線,我們需要使用如下系統呼叫來建立一個監聽佇列以存放待處理的客戶連線:
#include<sys/socket.h>
int listen(int sockfd, int lacklog); /*成功返回0,失敗返回-1*/
- sockfd:指定被監聽的socket。
- backlog:提示核心監聽佇列的最大長度。如果超過該長度,伺服器則不接受新的客戶連線
- 注意:在核心版本2.2之前的Linux中,backlog引數指的是所有處於半連線狀態和完全連線狀態的socket上限,2.2版本之後,只表示處於全連線狀態的socket上限。典型值為5.
四·發起連線
一般情況下,客戶端需要通過如下系統呼叫來主動與伺服器建立連線:
#include<sys.types.h>
#include<sys/socket.h>
int connect(int sockfd, const sockaddr* serv_addr, socklen_t addrlen);
/*成功返回0,失敗返回-1*/
- sockfd:由socket系統呼叫返回的一個socket。
- serv_addr:伺服器監聽的socket地址。
- addrlen:指定該地址的長度。
五·接受連線
我們使用下面這個系統呼叫來從listen的監聽佇列中接受一個連結:
#include<sys/types.h>
#include<sys/socket.h>
int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen);
/*成功返回一個新的連線socket,失敗返回-1*/
- sockfd:執行過listen系統呼叫的監聽socket。
- addr:用來獲取被接受的遠端socket地址。
- addelen:指向的記憶體中儲存著addr的大小
accept成功時返回一個新的連線socket,該socket唯一地標識了被接受的這個連線,伺服器可通過讀寫該socket來與被接受連線的客戶端進行通訊。
六·資料讀寫
tcp資料讀寫
對於檔案的讀寫操作read和write也適用於socket。但是socket程式設計介面提供了專門用於socket資料讀寫的系統呼叫,他們增加了對資料讀寫的控制,其中tcp流資料讀寫的系統呼叫是:
#include<sys/types.h>
#include<sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags); /*接收資料*/
ssize_t send(int sockfd, const void *buf, size_t len, int flags); /*傳送資料*/
- sockfd:recv讀取sockfd上的資料,send往sockfd上寫資料
- buf:指定讀寫緩衝區的位置
- size_t:指定讀寫緩衝區的大小
- flags:為資料收發提供了額外的控制,它可以取下面中的一個或者幾個的邏輯或。
udp資料讀寫
#include<sys/types.h>
#include<sys/socket.h>
ssize_t recvfrom(int sockfd, void* buf, size_t len, int flags, struct sockaddr* src_addr, socklen_t* len);
ssize_t sendto(int sockfd, const void* buf, size_t len, int flags, struct sockaddr* desr_addr, socklen_t addrlen);
- sockfd:recvfrom讀取sockfd上的資料,sendto往sockfd上寫入資料。
- buf:recvfrom指定讀緩衝區的位置,sendto指定寫緩衝區的位置。
- len:recvfrom指定讀緩衝區的大小,sendto指定寫緩衝區的大小。
- src_addr:因為udp通訊沒有連線的概念,所以我們每次讀取資料都需要獲取傳送端的socket地址,也就是該引數所指的內容
- dest_addr:指定接收端的socket地址。
- addrlen:分別指定傳送端和接收端socket地址的大小
- flags:每個函式中的flags都與send/recv中的flags含義相同
七·關閉socket
在程式結束時,我們使用close系統呼叫來關閉socket
int close(sockfd);
程式設計例項
tcp客戶端
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/un.h>
int main()
{
int sockfd = socket(PF_INET, SOCK_STREAM, 0);
assert(sockfd != -1);
struct sockaddr_in ser,cli;
ser.sin_family = AF_INET;
ser.sin_port = htons(6500);
ser.sin_addr.s_addr = inet_addr("127.0.0.1");
//int sockfd = 0;
int err = connect(sockfd, (struct sockaddr*)&ser, sizeof(ser));
if(err == -1)
{
printf("connect error!\n");
}
while(1)
{
printf("please input:");
fflush(stdout);
char buff[128] = {0};
fgets(buff, 127, stdin);
buff[strlen(buff)-1] = 0;
if(strncmp(buff,"end",3)==0)
{
break;
}
err = send(sockfd, buff, strlen(buff), 0);
assert(err != -1);
char recvbuff[128] = {0};
err = recv(sockfd, recvbuff, 127, 0);
assert(err = -1);
printf("%s\n",recvbuff);
}
close(sockfd);
return 0;
}
tcp伺服器
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<sys/un.h>
#include<netinet/in.h>
#include<arpa/inet.h>
int main()
{
int sockfd = socket(PF_INET, SOCK_STREAM, 0);
assert(sockfd != -1);
struct sockaddr_in ser,cli;
ser.sin_family = AF_INET;
ser.sin_port = htons(6500);
ser.sin_addr.s_addr = inet_addr("127.0.0.1");
int err = bind(sockfd, (struct sockaddr*)&ser, sizeof(ser));
assert(err != -1);
err = listen(sockfd, 5);
assert(err != -1);
while(1)
{
int len = sizeof(cli);
int c = accept(sockfd, (struct sockaddr*)&cli, &len);
if(c == -1)
{
printf("accept error\n");
continue;
}
while(1)
{
char buff[128] = {0};
err = recv(c, buff, 127, 0);
if(err <= 0)
{
printf("client disconnect!\n");
break;
}
printf("%s\n",buff);
err = send(c, "ok", 2, 0);
if(err == -1)
{
printf("error\n");
}
}
}
close(sockfd);
return 0;
}
udp客戶端
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/un.h>
int main()
{
int sockfd = socket(PF_INET, SOCK_DGRAM, 0);
assert(sockfd != -1);
struct sockaddr_in ser,cli;
ser.sin_family = AF_INET;
ser.sin_port = htons(6500);
ser.sin_addr.s_addr = inet_addr("127.0.0.1");
while(1)
{
char buff[128] = {0};
printf("please input:");
fflush(stdout);
fgets(buff, 127, stdin);
buff[strlen(buff)-1] = 0;
if(strcmp(buff,"end") == 0)
{
break;
}
int err = sendto(sockfd, buff, strlen(buff), 0, (struct sockaddr*)&ser, sizeof(ser));
assert(err != -1);
char recvbuff[128] = {0};
int len = sizeof(cli);
err = recvfrom(sockfd, recvbuff, 127, 0, (struct sockaddr*)&cli, &len);
assert(err != -1);
}
close(sockfd);
return 0;
}
udp伺服器
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/un.h>
int main()
{
int sockfd = socket(PF_INET, SOCK_DGRAM, 0);
assert(sockfd != -1);
struct sockaddr_in ser,cli;
ser.sin_family = AF_INET;
ser.sin_port = htons(6500);
ser.sin_addr.s_addr = inet_addr("127.0.0.1");
int err = bind(sockfd,(struct sockaddr*)&ser,sizeof(ser));
assert(err != -1);
while(1)
{
char buff[128] = {0};
int len = sizeof(cli);
err = recvfrom(sockfd, buff, 127, 0, (struct sockaddr*)&cli, &len);
if(err <= 0)
{
continue;
}
printf("%s\n",buff);
err = sendto(sockfd, "ok", 2, 0, (struct sockaddr*)&cli, sizeof(cli));
assert(err != -1);
}
close(sockfd);
return 0;
}