1. 程式人生 > >socket網路程式設計基礎篇

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 listenintsocketfdintbacklog);

//此函式呼叫後,當客戶端呼叫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代表longs代表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;
}