歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux基礎 >> Linux技術 >> 【Socket編程】篇一

【Socket編程】篇一

日期:2017/3/3 12:50:11   编辑:Linux技術
參考自:/content/3615335.html(吳秦)
1、Socket 簡介
本地的進程間通信(IPC)有多種方法:
1)消息傳遞(PIPE、FIFO、消息隊列等)
2)同步(互斥量、條件變量、讀寫鎖、記錄鎖、信號量等)
3)共享內存(匿名的和具名的)
4)遠程過程調用(Solaris門和Sun RPC)
在本地可以通過進程 PID 來標識一個進程,但是在網絡中這是行不通的。其實 TCP/IP 協議族已經幫我們解決了這個問題,網絡層的 “ip地址” 可以唯一標識網絡中的主機,而傳輸層的 “協議+端口” 可以唯一標識主機中的應用程序(進程)。這樣利用三元組(ip地址,協議,端口)就可以標識網絡的進程了,網絡中的進程通信就可以利用這個標志與其它進程進行交互。
就目前而言,幾乎所有的應用程序都是采用 socket 進行網絡編程。
自學的過程中發現這個在線課堂,還不錯~
http://www.hubwiz.com/course/56f9ee765fd193d76fcc6c17/(匯智網《Linux網絡編程入門》)
2、Socket基本操作
socket是 “open—write/read—close” 模式的一種實現,那麼socket就提供了這些操作對應的函數接口。下面以TCP為例,介紹幾個基本的socket接口函數。
2.1 socket()
#include<sys/socket.h>
int socket(int domain, int type, int protocol);

socket() 函數對應於普通文件的打開操作。普通文件的打開操作返回一個文件描述符,而 socket() 用於創建一個 socket 描述符(socket descriptor),它唯一標識一個socket。socket 函數的三個參數分別為:
1)domain:即協議域,又稱為協議族(Address Family)。常用的協議族有,AF_INET、AF_INET6、AF_LOCAL(或稱AF_UNIX,Unix域socket)、AF_ROUTE等等。協議族決定了socket的地址類型,在通信中必須采用對應的地址,如
AF_INET 決定了要用 ipv4 地址(32位的)與端口號(16位的)的組合、AF_UNIX 決定了要用一個絕對路徑名作為地址。
2)type:指定socket類型。常用的socket類型有:SOCK_STREAM(流服務,適用TCP協議)、SOCK_DGRAM(數據包服務,使用UDP協議)、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET 等等。
3)protocol:故名思意,就是指定協議。常用的協議有,IPPROTO_TCP、IPPROTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它們分別對應
TCP傳輸協議、UDP傳輸協議、SCTP傳輸協議、TIPC傳輸協議。
注意:並不是上面的 type 和 protocol 可以隨意組合的,如 SOCK_STREAM 不可以跟 IPPROTO_UDP 組合。當protocol為0時,會自動選擇type類型對應的默認協議。函數執行成功返回一個socket文件描述符,失敗返回-1
當我們調用socket創建一個socket時,返回的socket描述符存在於協議族(address family,AF_XXX)空間中,但沒有一個具體的地址。如果想要給它賦值一個地址,就必須調用bind()函數,否則就當調用connect()、listen()時系統會自動隨機分配一個端口。
2.2 bind()
正如上面所說, bind() 函數把一個地址族中的特定地址賦給socket。例如對應AF_INET、AF_INET6 就是把一個
ipv4 或 ipv6 地址和端口號組合賦給socket。
#include<sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

函數的三個參數分別為:
1)sockfd:即socket描述符,它通過socket()函數創建,標識唯一的socket。bind()函數就是給這個描述符綁定一個地址。
2) addr:一個const struct sockaddr *指針,指向要綁定給 sockfd 的協議地址。這個地址結構根據地址創建socket時的地址協議族的不同而不同。
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 */
};

ipv6對應的是:
struct sockaddr_in6 { 
    sa_family_t     sin6_family;   /* AF_INET6 */ 
    in_port_t       sin6_port;     /* port number */ 
    uint32_t        sin6_flowinfo; /* IPv6 flow information */ 
    struct in6_addr sin6_addr;     /* IPv6 address */ 
    uint32_t        sin6_scope_id; /* Scope ID (new in 2.4) */ 
};

struct in6_addr { 
    unsigned char   s6_addr[16];   /* IPv6 address */ 
};

Unix域對應的是:
#define UNIX_PATH_MAX    108

struct sockaddr_un { 
    sa_family_t sun_family;               /* AF_UNIX */ 
    char        sun_path[UNIX_PATH_MAX];  /* pathname */ 
};

3)addrlen:對應的是地址的長度。
函數執行成功返回0,失敗返回-1。
通常服務器在啟動的時候都會綁定一個眾所周知的地址(如ip地址+端口號),用於提供服務,客戶就可以通過它來接連服務器;而客戶端就不用指定,由系統自動分配一個端口號和自身的ip地址組合。這就是為什麼通常服務器端在listen之前會調用bind(),而客戶端就不會調用,而是在connect()時由系統隨機生成一個。
2.3 listen()、connect()
作為一個服務器,在調用socket()、bind()之後就會調用listen()來監聽這個socket,如果客戶端這時調用connect()發出連接請求,服務器端就會接收到這個請求。
int listen(int sockfd, int backlog);

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

listen() 函數的第一個參數即為要監聽的 socket 描述符,第二個參數為相應 socket 監聽隊列的最大長度。socket()函數創建的socket默認是一個主動類型的,listen() 函數將socket變為被動類型的,等待客戶的連接請求。listen() 函數創建一個監聽隊列以存放待處理的客戶連接,將套接字sockfd指定為被監聽的socket,其中backlog一般取值為5。
connect() 函數的第一個參數即為客戶端的 socket 描述符,第二參數為服務器的socket地址,第三個參數為socket地址的長度。客戶端通過調用 connect() 函數來建立與 TCP 服務器的連接,如果連接成功後,其第一個參數sockfd就唯一標示這個連接。函數執行成功返回0,失敗返回-1。
2.4 accept()
TCP服務器端依次調用socket()、bind()、listen()之後,就會監聽指定的socket地址了。TCP客戶端依次調用socket()、connect()之後就想TCP服務器發送了一個連接請求。TCP服務器監聽到這個請求之後,就會調用accept()函數取接收請求,這樣連接就建立好了。之後就可以開始網絡I/O操作了,即類同於普通文件的讀寫I/O操作。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

accept() 函數的第一個參數為服務器的 socket描述符,第二個參數為指向struct sockaddr *的指針,用於返回客戶端的協議地址,第三個參數為協議地址的長度。如果 accpet 成功,那麼其返回值是由內核自動生成的一個全新的描述字,代表與返回客戶的TCP連接。
注意:accept() 的第一個參數為服務器的socket描述符,是服務器開始調用socket()函數生成的,稱為監聽socket描述符;而accept函數返回的是已連接的socket描述符。一個服務器通常通常僅僅只創建一個監聽socket描述符,它在該服務器的生命周期內一直存在。內核為每個由服務器進程接受的客戶連接創建了一個已連接socket描述符,當服務器完成了對某個客戶的服務,相應的已連接socket描述符就被關閉。
2.5 read()、write()
至此服務器與客戶已經建立好連接了。可以調用網絡I/O進行讀寫操作了,即實現網絡中不同進程之間的通信。網絡I/O操作有下面幾組:
1)read()/write()
2)recv()/send()
3)readv()/writev()
4)recvmsg()/sendmsg()
5)recvfrom()/sendto()
2.6 close()
在服務器與客戶端建立連接之後,會進行一些讀寫操作,完成了讀寫操作就要關閉相應的socket描述符,好比操作完打開的文件要調用fclose關閉打開的文件。
int close(int fd);

close一個TCP socket的缺省行為是把該socket標記為已關閉,然後立即返回到調用進程。該描述符不能再由調用進程使用,也就是說不能再作為read或write的第一個參數。
注意:close操作只是使相應socket描述符的引用計數-1,只有當引用計數為0的時候,才會觸發TCP客戶端向服務器發送終止連接請求。
3、C/S模型
C/S模型(即client/server, 客戶端/服務端模型),是最經典的服務器模型,使用TCP連接,模型如下圖所示。 TCP/IP協議在設計和實現並沒有區分客戶端和服務端,通信雙方其實地位相同,但是因為很多數據(比如新聞、音樂、視頻)均由公司集中提供,比如我們看視頻的時候,服務端負責提供相關視頻資源,因此有了客戶端和服務端之分,很多客戶端可以訪問服務端以獲取資源。
采用C/S模型的TCP客戶端和TCP服務器工作流程,如下圖所示:

服務端工作流程如下:
1)調用socket函數創建套接字(socket)。
2)調用bind函數給創建的套接字,分配IP地址和端口(bind)。
3)調用listen函數進行監聽,等待客戶端連接(listen)。
4)等待客戶請求到來: 當請求到來後,調用accept函數接受連接請求,返回一個對應於此次連接的新的套接字,做好相互通信准備(accept)。
5)調用write/read或send/recv進行數據的讀寫,通過accept返回的套接字和客戶端進行通信.
6)關閉socket(close)。
客戶端工作流程如下:
1)調用socket函數創建套接字(socket)。
2)調用connect函數連接服務端(connect)。
3)調用write/read或send/recv進行數據的讀寫。
4)關閉socket(close)。
4、TCP三次握手
“三次握手”大致流程如下:
1)客戶端向服務器發送一個SYN J;
2)服務器向客戶端響應一個ACK J + 1,並發送一個 SYN K;
3)客戶端再向服務器發一個確認ACK K+1。
如下圖:

從圖中可以看出,當客戶端調用connect時,觸發了連接請求,向服務器發送了SYN J包,這時connect進入阻塞狀態;服務器監聽到連接請求,即收到SYN J包,調用accept函數接收請求向客戶端發送SYN K ,ACK J+1,這時accept進入阻塞狀態;客戶端收到服務器的SYN K ,ACK J+1之後,這時connect返回,並對SYN K進行確認;服務器收到ACK K+1時,accept返回,至此三次握手完畢,連接建立。
總結:客戶端的connect在三次握手的第二個次返回,而服務器端的accept在三次握手的第三次返回。
5、TCP四次揮手
Socket中的四次握手釋放連接的過程,請看下圖:

某個應用進程首先調用close主動關閉連接,這時TCP發送一個FIN M;另一端接收到FIN M之後,執行被動關閉,對這個FIN進行確認。它的接收也作為文件結束符傳遞給應用進程,因為FIN的接收意味著應用進程在相應的連接上再也接收不到額外數據;一段時間之後,接收到文件結束符的應用進程調用close關閉它的socket。這導致它的TCP也發送一個FIN N;接收到這個FIN的源發送端TCP對它進行確認。
6、動動手
下面編寫一個簡單的服務器、客戶端(使用TCP)——服務器端一直監聽本機的7777號端口,如果收到連接請求,將接收請求並接收客戶端發來的消息;客戶端與服務器端建立連接並發送一條消息。
服務器:
#include <cstdio>
#include <unistd.h>
#include <stdlib.h>
#include <cstring>
#include <cassert>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

const int BUFFER_SIZE = 4096;
const int SERVER_PORT = 7777;

int main()
{
	int server_socket;
	char buff[BUFFER_SIZE];
	int n;

	server_socket = socket(AF_INET, SOCK_STREAM, 0);
	assert(server_socket != -1);

	struct sockaddr_in server_addr;
	memset(&server_addr, 0, sizeof(server_addr));
	server_addr.sin_family = AF_INET;
	server_addr.sin_port = htons(SERVER_PORT);
	server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

	assert(bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) != -1);
	assert(listen(server_socket, 5) != -1);
	
	struct sockaddr_in client_addr;
	socklen_t client_addr_len = sizeof(client_addr);

	while(1)
	{
		printf("waiting...\n");
		int connfd = accept(server_socket, (struct sockaddr*)&client_addr, &client_addr_len);
		printf("here\n");
		if(connfd == -1)
			continue;
		n = recv(connfd, buff, BUFFER_SIZE, 0);
		buff
 = '\n';
		printf("recv msg from client: %s\n", buff);
		close(connfd);
	}
	close(server_socket);

	return 0;
}

客戶端:
#include <cstdio>
#include <unistd.h>
#include <stdlib.h>
#include <cstring>
#include <cassert>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

const int BUFFER_SIZE = 4096;
const int SERVER_PORT = 7777;

int main()
{
	int client_socket;
	const char *server_ip = "127.0.0.1";
	char buff[BUFFER_SIZE] = "I'm from client!\n";

	client_socket = socket(AF_INET, SOCK_STREAM, 0);
	assert(client_socket != -1);

	struct sockaddr_in server_addr;
	memset(&server_addr, 0, sizeof(server_addr));
	server_addr.sin_family = AF_INET;
	server_addr.sin_port = htons(SERVER_PORT);
	server_addr.sin_addr.s_addr = inet_addr(server_ip);

	assert(connect(client_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) != -1);
	assert(send(client_socket, buff, strlen(buff), 0) != -1);

	close(client_socket);

	return 0;
}
Copyright © Linux教程網 All Rights Reserved