socket網路程式設計基礎篇
首先列舉一下socket網路通訊的例子:使用區域網打遊戲,用瀏覽器連線外網看視訊,使用QQ與好友通訊,手機連線wifi傳資料等等。socket是底層抽象給應用層所使用的一套介面函式,本篇講解這些函式的使用。
物件:1、伺服器server(等待客戶端連線)
2、客戶端client(主動連線伺服器)
物件之間的聯絡:
client是根據server的‘’ip地址+埠號”找到對方並建立連線的
1、ip地址:不用說了,就是192.168.6.xxx之類(一個主機可能有多個ip)。
2、埠:同一個ip下又可分為多個埠,做個比喻吧:ip相當於一個大別墅,多個埠相當於
別墅裡的多個房間,資料就相當於客人,客人可以進不同的房間幹不同的事情(即業務)。
傳輸方式:
1、TCP(資料可靠,一般常用這種)
2、UDP(資料不可靠,一般用於實時視訊傳輸)
server(伺服器必要程式碼):
1、fd = socket(int domain, int type, int protocol);
//相當於獲得了一個標誌(fd就是這個伺服器了),以後想用這個伺服器就去找fd就行了
●domain:協議域或協議族,例如AF_INET、AF_INET6、AF_LOCAL等,其決定了socket的地址型別,例如我們常用的AF_INET決定了要用ipv4地址(32位)+埠號(16位)的組合。
●type:指定socket型別,常用的有SOCK_STREAM、SOCK_DGRAM、SOCK_RAM等等
●protocal:指定協議,TCP協議、UDP協議、STCP協議、TIPC協議
//注意:並不是上面的type和protocol可以隨意組合的,如SOCK_STREAM不可以跟IPPROTO_UDP組合。設定protocol為0時,會自動選擇type型別對應的預設協議。
2、int blind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
//blind翻譯為繫結,就是將上面socket()出來的標誌fd與真實伺服器的地址進行繫結,因為人家是要連線這個地址,繫結後fd才真正的成為了這個地址(伺服器)的代言人!
●sockfd:就是那個fd(伺服器的代言人)
●addr:要綁的地址(伺服器的ip和埠),所以在呼叫blind函式之前需要先設定這個結構體
要注意的是這個地址根據建立socket時的協議族的不同而不同。
小技巧:用man 7ip迅速查詢到並粘貼出來
//對應ipv4格式的地址如下所示:
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* internet address */
};
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
//注意:這裡發現blind函式的引數2是sockaddr結構,但是ipv4的是sockaddr_in結構,所以要做一個強制型別轉換成通用的sockaddr結構(其他ipv6等等也都要這樣做)
●addrlen:對應地址的長度
●返回值:成功返回0,失敗返回-1
//注意:這個函式是伺服器獨有的,客戶端不需要,因為客戶端在呼叫connect函式的時候系統會自動分配一個本機ip+埠給他。
3、int listen(intsocketfd,intbacklog);
//此函式呼叫後,當客戶端呼叫connect函式發出連線請求時,伺服器端會收到此請求。
//且listen函式一旦呼叫,此fd將變成被動套接字(今後只能等待別人來連線,而不能主動連)
//內部維護了兩個佇列:1、已由客戶發出併到達伺服器,伺服器正在等待完成相應的TCP三路握手
2、已完成連線的佇列
//後續呼叫的accept函式(繼續往後看)會從第二個佇列中取出一個連線
●socketfd:就是那個fd(伺服器的代言人)
●backlog:排隊的最大連線個數
●返回值:0成功,-1失敗
4、int accept(int sockfd, structsockaddr *addr, socklen_t *addrlen);
//從已完成連線佇列(即listen內部維護的佇列)返回第一個連線,如果已完成連線佇列為空,則阻塞。
●sockfd:就是那個fd(伺服器的代言人)
●addr:獲得對方的地址存在此結構中(客戶端的地址)
●addrlen:地址長度
●返回值:成功返回客戶端的fd(客戶端代言人),失敗返回-1
5、read()/write() 或者 recv()/send()
ssize_tread(intfd,void *buf,size_tcount);
ssize_twrite(intfd,void *buf,size_tcount);
ssize_trecv(intsockfd,void *buf,size_tlen,intflags);
ssize_tsend(intsockfd,constvoid *buf,size_tlen,intflags);
//共同點:這兩套讀寫函式都可以實現資料的收發。
//區別:1、read函式可用於檔案/套接字/標準輸入輸出,而recv只能用於套接字
// 2、recv()函式多了個引數flag;//flag可取值:MSG_OOB(帶外資料 緊急指標)
// MSG_PEEK(資料包的提前預讀)
// flag取0則等同於read函式
client(客戶端必要程式碼):
1、fd = socket();//獲得客戶端代言人fd
//函式講解、函式引數同server,略。
2、int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
//將客戶端連線到伺服器,呼叫connect函式後伺服器的accept函式會收到這個連線的
●sockfd:就是那個fd(客戶端的代言人)
●addr:要連線的伺服器的地址(在呼叫connect之前要填充這個地址的結構體!)
●addrlen:地址長度
●返回值:成功返回0,失敗返回-1
3、read()/write() 或者 recv()/send()
//函式講解、函式引數同server,略。
其他程式碼
1、位元組序轉換程式碼:
問:位元組序是什麼?為什麼要轉換位元組序?
答:由於進行網路傳輸的雙方不一定在同一個主機上,可能是PC----PC
或者PC----ARM...等等不同架構之間通訊,而存在大端和小端的說法。
1、大端:低位存放於高記憶體地址處
2、小端:低位存放於低記憶體地址處
測試自己主機是大端還是小端方法:
void main()
{
unsigned int data = 0x12345678;
char *p = &data;
printf(“%x %x %x %x \n”,p[0],p[1],p[2],p[3]);
if(p[0] == 0x78)
{
printf(“當前系統是小端模式”);
}
else
{
printf(“當前系統是大端模式”);
}
}
在x86下測試列印得出:
78 56 34 12
當前系統是小端模式
在大端(Big Endian)和小端(Little Endian)中,資料在記憶體中的存放順序是不一樣的,例如在大端的A主機上的記憶體裡一個數據,將其傳送給小端的B主機,存在記憶體中順序就錯了,這對socket傳輸是致命的,所以需要解決這種問題:
解決方法:
1、在傳送之前先轉換成網路位元組序(網路位元組序為大端)
2、然後接收端收到的是網路位元組序
3、接收端將網路位元組序轉換成本地位元組序即可(不同主機不同,例:x86位小端、ARM可配置)
引出了了一系列位元組序轉換函式:
uint32_t htonl(uint32_t hostlong);//主機位元組序轉為網路位元組序
uint16_t htons(uint16_t hostshort);//主機位元組序轉為網路位元組序
uint32_t ntohl(uint32_t netlong);//網路位元組序轉為主機位元組序
uint16_t ntohs(uint16_t netshort);//網路位元組序轉為主機位元組序
說明:在上述的函式中,h代表host主機;n代表network s代表short;l代表long;s代表short
2、地址轉換程式碼:
問:為什麼需要地址轉換?
答:比如客戶端要連線伺服器的ip是192.168.6.112,客戶端要先把這個ip轉換為一個32位的 ipv4然後再去連線
引出了了一系列地址轉換函式:
int inet_aton(const char *cp, struct in_addr *inp);//192.168.6.xx====>in_addr結構
in_addr_t inet_addr(const char *cp); //192.168.6.xx====>in_addr結構
char *inet_ntoa(struct in_addr in); //in_addr結構====>192.168.6.xx
到此基礎講解完畢,下面貼程式碼
程式碼功能:
客戶端從控制檯鍵盤輸入併發送給伺服器,伺服器收到資料並打印出客戶端資訊和資料。
伺服器端用了fork,即可支援多客戶端連線。
server.c:
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <signal.h>
#define SERVER_PORT 8888 //埠號,定義為巨集方便以後直接修改
#define BACKLOG 10 //表伺服器可以同時監測多少個客戶端連線,設定為>0即可
int main(int argc, char **argv)
{
int iSocketServer;
int iSocketClient;
struct sockaddr_in tSocketServerAddr;//伺服器地址結構
struct sockaddr_in tSocketClientAddr;//客戶端地址結構:後來當客戶端來連線時會傳過來
int iRet;
int iAddrLen;
int iRecvLen;
unsigned char ucRecvBuf[1000];//伺服器收到資料的緩衝區
int iClientNum = -1;
/*
*防止殭屍程序,子程序結束後還是會存於程序表項中,可用ps -u book(使用者)檢視到
*所以要傳送一個訊號SIGCHLD給父程序,讓其給它收屍(注:所有64個訊號可由kill -l檢視)
*SIG_IGN為忽略的意思,可讓核心把殭屍程序轉交給init程序去處理,防止其佔用系統資源
*/
signal(SIGCHLD,SIG_IGN);/*防止殭屍程序:子程序退出後會給父程序一個訊號,然後來給它收屍即可*/
iSocketServer = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == iSocketServer)
{
printf("socket error!\n");
return -1;
}
tSocketServerAddr.sin_family = AF_INET;
tSocketServerAddr.sin_port = htons(SERVER_PORT); /* 埠是2個位元組即short型 */
tSocketServerAddr.sin_addr.s_addr = INADDR_ANY;/*本機上所有IP,若為特定ip的話需要用inet_addr()函式來轉換一下*/
memset(tSocketServerAddr.sin_zero, 0, 8);/*設定為0----8位元組*/
iRet = bind(iSocketServer, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr));
/* 注:上面函式第二個引數強制型別轉換為通用的sockaddr結構(因為sockaddr_in結構是) */
if (-1 == iRet)
{
printf("bind error!\n");
return -1;
}
iRet = listen(iSocketServer, BACKLOG);
if (-1 == iRet)
{
printf("listen error!\n");
return -1;
}
while (1)
{
iAddrLen = sizeof(struct sockaddr);
iSocketClient = accept(iSocketServer, (struct sockaddr *)&tSocketClientAddr, &iAddrLen);/* 引數2獲得了對方的IP地址 */
if (-1 != iSocketClient)
{
iClientNum++;
printf("Get connect from client %d : %s\n", iClientNum, inet_ntoa(tSocketClientAddr.sin_addr));
/*
*在父程序中呼叫fork()返回子程序的PID號,取非則變為0,所以直接跳過if,轉到while開頭繼續accept新的客戶端
*而子程序的fork返回0,則繼續進去執行
*/
if (!fork())
{
/* 子程序 */
while (1)
{
/* 接收客戶端發來的資料並顯示出來 */
iRecvLen = recv(iSocketClient, ucRecvBuf, 999, 0);
if (iRecvLen <= 0)
{
//如果在讀的過程中對方關閉,tcpip協議會返回一個0資料包
close(iSocketClient);
return -1;
}
else
{
ucRecvBuf[iRecvLen] = '\0';
printf("Get Msg From Client %d: %s\n", iClientNum, ucRecvBuf);
}
}
}
}
}
close(iSocketServer);
return 0;
}
client.c:
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#define SERVER_PORT 8888
int main(int argc, char **argv)
{
int iSocketClient;
struct sockaddr_in tSocketServerAddr;
int iRet;
unsigned char ucSendBuf[1000];//傳送緩衝區
int iSendLen;
if (argc != 2)
{
printf("Usage:\n");
printf("%s <server_ip>\n", argv[0]);//引數不為2個就列印用法
return -1;
}
iSocketClient = socket(AF_INET, SOCK_STREAM, 0);
tSocketServerAddr.sin_family = AF_INET;
tSocketServerAddr.sin_port = htons(SERVER_PORT);
if (0 == inet_aton(argv[1], &tSocketServerAddr.sin_addr))
{
printf("invalid server_ip\n");
return -1;
}
memset(tSocketServerAddr.sin_zero, 0, 8);//結構體後8位為保留位,清0
iRet = connect(iSocketClient, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr));
if (-1 == iRet)
{
printf("connect error!\n");
return -1;
}
while (1)
{
if (fgets(ucSendBuf, 999, stdin))/*從stdin獲得資料(我們自己實時敲入的)到ucSendBuf*/
{
iSendLen = send(iSocketClient, ucSendBuf, strlen(ucSendBuf), 0);
if (iSendLen <= 0)
{
close(iSocketClient);
return -1;
}
}
}
return 0;
}