柳大的Linux講義·基礎篇(4)網路程式設計基礎
柳大的Linux遊記·基礎篇(4)網路程式設計基礎
- Author: 柳大·Poechant
- Blog:Blog.CSDN.net/Poechant
- Email:zhongchao.usytc#gmail.com (#->@)
- Date:March 11th, 2012
- Copyright © 柳大·Poechant(鍾超·Michael)
回顧
閒話
最近很忙,博文寫的少。感謝一些博友的理解。
有博友發郵件說《柳大的Linux遊記》希望繼續寫下去,希望瞭解一些與 socket 入門有關的內容。對此深表慚愧,當時也是應一個博友的來信而開始寫這個系列的,但僅寫了三篇就沒繼續了。與 socket 相關的文章,在網路上非常多,socket 程式設計也是基本功。要我來寫的話,寫出心意很難,我只希望能寫系統一些,所以我想先介紹 socket 的基礎,然後說說 select,poll 和 epoll 等 IO 複用技術,可能這樣會系統一些,也更實用。
W. Richard Stevens的UNIX Network Programming Volume 1中講解例子的時候都使用了include "unp.h"
,這是Stevens先生在隨書原始碼中的提供的一個包含了所有 UNIX 網路程式設計會用到的標頭檔案的的一個標頭檔案。但這樣對於不瞭解 UNIX 網路程式設計以及 socket 的朋友來說,並不是一個好的學習途徑。所以我想看完本文後讀Stevens先生的傑出作品更好一些 :)
另外,《JVM深入筆記》的第四篇正在整理,最近確實空閒時間比較少,對此感到很抱歉。我會盡量抽時間多分享一些的。
言歸正傳,下面還是沿襲我的一貫風格,先以最簡單的例項開始。
目錄
- 快速開始
- 1.1 TCP C/S
- 1.1.1 TCP Server
- 1.1.2 TCP Client
- 1.2. UCP C/S
- 1.2.1 UDP Server
- 1.2.2 UDP Client
- 1.1 TCP C/S
- TCP 和 UCP 的 Socket 程式設計對比
- 2.1 Server
- 2.2 Client
- 2.3 所使用的 API 對比
- 裸用 socket 的效能很差
1 快速開始
1.1 TCP C/S
無論你是使用 Windows 還是 UNIX-like 的系統,作業系統提供給應用層的網路程式設計介面都是 Socket。在 5 層的 TCP/IP 網路結構或者 7 層的 OSI 網路結構中,都有傳輸層,TCP 和 UDP 協議就是為傳輸層服務的。而網路層的最常用協議就是 IP(IPv4 或 IPv6)在高層編寫程式,就需要用到 TCP 協議和 UDP 協議。其直接使用,就是通過 Socket 來實現的。
先看一段簡單的 TCP 通訊的 Server 與 Client 例程。
1.1.1 TCP Server
下面是一個 TCP 連線的 Server 例項。
#include <string.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
// Get Port option
if (argc < 2)
{
fprintf(stderr, "ERROR, no port provided\n");
exit(1);
}
int port_no = atoi(argv[1]);
// Get socket
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
// Bind
struct sockaddr_in server_addr;
bzero((char *) &server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(port_no);
bind(socket_fd, (struct sockaddr *) &server_addr, sizeof(server_addr));
// Listen
listen(socket_fd, 5);
while (1) {
// Accept
struct sockaddr_in client_addr;
socklen_t client_addr_length = sizeof(client_addr);
int socket_to_client = accept(socket_fd, (struct sockaddr *) &client_addr, &client_addr_length);
// Read
char buffer[1024];
bzero(buffer, sizeof(buffer));
read(socket_to_client, buffer, sizeof(buffer) - 1);
printf("Here is the message: %s\n", buffer);
// Write
const char *data = "I got your message.";
write(socket_to_client, data, sizeof(data));
// Close
close(socket_to_client);
}
close(socket_fd);
return 0;
}
上面是 TCP 的 Client 的 Simplest Example。概括起來 Scoket Server 程式設計有如下幾個步驟:
1.1.1.1 獲取 Socket Descriptor:
// socket function is included in sys/socket.h
// AF_INET is included in sys/socket.h
// SOCK_STREAM is included in sys/socket.h
socket(AF_INET, SOCK_STREAM, 0);
通過sys/socket.h
中的socket
函式。第一個參數列示使用IPv4 Internet Protocol
,如果是AF_INET6
則表示IPv6 Internet Protocol
,其中AF
表示Address Family
,另外還有PF
表示Protocol Family
。第二個參數列示流傳輸Socket Stream
,流傳輸是序列化的、可靠的、雙向的、面向連線的,Kernel.org 給出的解釋是:“Provides sequenced, reliable, two-way, connection-based byte streams. An out-of-band data transmission mechanism may be supported.”
另外一個常用的是SOCK_DGRAM
表示Socket Diagram
,是無連線的、不可靠的傳輸方式,Kernel.org 給出的解釋是“Supports datagrams (connectionless, unreliable messages of a fixed maximum length).”
第三個參數列示使用的協議族中的哪個協議。一般來說一個協議族經常只有一個協議,所以長使用“0”。具體參見Kernel.org 給出的解釋。
1.1.1.2 繫結地址與埠
首先要建立一個struct sockaddr_in
,並設定地址族、監聽的外來地址與本地埠號。如下:
// struct sockaddr_in is inclued in netinet/in.h
// bzero function is included in string.h
// atoi is include in stdlib.h
struct sockaddr_in server_addr;
bzero((char *) &server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(atoi(argv[1]))
然後將第 1 步建立的Socket
與這裡建立的地址繫結(實際上直接用的是Socket Descriptor
)。
// struct sockaddr is included in sys/socket.h
// bind function is included in sys/socket.h
bind(socket_fd, (struct sockaddr *) &server_addr, sizeof(server_addr));
1.1.1.3 開始監聽
// listen function is included in sys/socket.h
listen(socket_fd, 5);
第一個引數不用贅述了,第二個引數是連線佇列(connection queue)的最大長度。當Server
進行了Accept
後,在新來的請求就得進入佇列等待,如果佇列滿了,再來的連線就會被拒絕。
1.1.1.4 接受連線
// struct sockaddr_in is included in netinet/in.h
// accept function is included in sys/socket.h
struct sockaddr_in client_addr;
socklen_t client_addr_length = sizeof(client_addr);
int socket_to_client = accept(socket_fd, (struct sockaddr *) &client_addr, &client_addr_length);
在開始監聽socket_fd
後,接收來自該Socket
的連線,將獲取到的客戶端地址和地址長度寫入client_addr
和client_addr_length
中。該accept
在成功接受某連線後會得到該連線的Socket
,並將其Socket Descriptor
返回,就得到了socket_to_client
。
1.1.1.5 接收和傳送資料
// bzero function is included in string.h
// read function is included in unistd.h
char buffer[1024];
bzero(buffer, sizeof(buffer));
read(socket_to_client, buffer, sizeof(buffer) - 1);
在接受連線後,就可以從該Socket
讀取客戶端傳送來的資料了,資料讀取到char *
的字串中。傳送過程也類似。
// write function is included in unistd.h
const char *data = "Server has received your message.";
write(socket_to_client, data, sizeof(data));
1.1.1.6 關閉Socket
// close function is included in unistd.h
close(socket_fd);
以上就簡單解釋了 TCP Server 的 Socket 通訊過程。簡單概括如下:
Create Socket - Bind socket with port - Listen socket - Accept connection - Read/Write - Close
1.1.2 TCP Client
再來看看 Client。以下是例程:
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <netdb.h> // bcopy
#include <stdlib.h>
int main(int argc, char *argv[])
{
// Get options
if (argc < 3)
{
fprintf(stderr, "Usage: %s <hostname> <port>\n", argv[0]);
exit(1);
}
struct hostent *server_host = gethostbyname(argv[1]);
int server_port = atoi(argv[2]);
// Get socket
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
// Connect
struct sockaddr_in server_addr;
bzero((char *) &server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
bcopy((char *) server_host->h_addr, (char *) &server_addr.sin_addr.s_addr, server_host->h_length);
server_addr.sin_port = htons(server_port);
connect(socket_fd, (struct sockaddr *) &server_addr, sizeof(server_addr));
// Input
printf("Please enter the message: ");
char buffer[1024];
bzero(buffer, sizeof(buffer));
fgets(buffer, sizeof(buffer) - 1, stdin);
// Write
write(socket_fd, buffer, strlen(buffer));
// Read
bzero(buffer, sizeof(buffer));
read(socket_fd, buffer, sizeof(buffer) - 1);
printf("%s\n", buffer);
// Close
close(socket_fd);
return 0;
}
上面是 TCP 的 Client 的 Simplest Example。概括起來 Scoket Client 程式設計有如下幾個步驟:
1.1.2.1 獲取 Socket Descriptor:
與 Server 一樣。
1.1.2.2 連線伺服器
// struct sockaddr_in is included in netinet/in.h
// struct sockaddr is included in sys/socket.h
// bzero is included in string.h
// bcopy is included in string.h
// AF_INET is included in sys/socket.h
// connect is included in sys/socket.h
connect(socket_fd, (struct sockaddr *) &server_addr, sizeof(server_addr));
第一個引數是int
型的Socket Descriptor
,通過socket
函式得到。第二個引數是指向struct sockaddr
地址的指標,第三個引數是該地址的大小。而這個地址是通過struct sockaddr_in
得到的,然後用bzero
位初始化,再賦初值,包括地址族為AF_INET
,地址為struct sockaddr_in
中的h_addr
,這裡用到了bcopy
位拷貝函式,最後再賦上埠號htons(int server_port)
。connect
的作用,就是將本地的Socket
與伺服器建立連線,而這個Socket
則是通過Socket Descriptor
來標示的。
1.1.2.3 傳送或接收資料
首先看傳送資料:
// write function is included in unistd.h
char buffer[1024]
...
write(socket_fd, buffer, strlen(buffer));
然後用write
函式,向Socket
所連線的伺服器傳送資料,資料是char *
的字串。再看下面的接收資料:
// read function is included in unistd.h
read(socket_fd, buffer, sizeof(buffer) - 1);
第一個引數是Socket Descriptor
,第二個引數是char *
的字串,長度為第三個引數標示的sizeof(buffer)-1
。功能就是從socket_fd
標示的Socket
所連線的伺服器讀取資料。
1.1.2.4 關閉Socket
// close function is included in unistd.h
close(socket_fd);
以上就簡單解釋了客戶端的最基本的Socket
通訊。概括起來的過程就是:
Create Socket - Connect socket with server - Write/Read - Close
1.2 UDP C/S
剛才介紹了最簡單的 TCP C/S 模型,下面看看 UDP C/S 模型。
1.2.1 UDP Server
下面是 UDP 連線的 Server 例項:
#include "sys/socket.h"
#include "netinet/in.h"
#include "string.h"
int main(int argc, char *argv[])
{
// Create Socket
int socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
// Bind
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(atoi(argv[1]));
bind(socket_fd, (struct sockaddr *) &server_addr, sizeof(server_addr));
// As accept
while (1)
{
// Receive
char buffer[1024];
struct sockaddr_in client_addr;
socklen_t client_addr_length = sizeof(client_addr);
int msg_recv_length = recvfrom(socket_fd, buffer, sizeof(buffer), 0, (struct sockaddr *) &client_addr, &client_addr_length);
// Write
const char *msg_send = "Received a datagram: ";
write(1, msg_send, strlen(msg_send));
write(1, buffer, msg_recv_length);
// Send
const char *msg_send_2 = "Got your message\n";
sendto(socket_fd, msg_send_2, strlen(msg_send_2), 0, (struct sockaddr *) &client_addr, sizeof(struct sockaddr_in));
}
close(socket_fd);
return 0;
}
UDP Server 的建立主要有以下幾步:
1.2.1.1 獲取 Socket Descriptor
sock=socket(AF_INET, SOCK_DGRAM, 0);
建立Socket
,獲取Socket Descriptor
。
1.2.1.2 繫結地址與埠
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(atoi(argv[1]));
bind(socket_fd, (struct sockaddr *) &server_addr, sizeof(server_addr));
將埠和允許的地址與Socket
繫結。
1.2.1.3 接收和傳送資料
char buffer[1024];
struct sockaddr_in client_addr;
socklen_t client_addr_length = sizeof(client_addr);
int msg_recv_length = recvfrom(socket_fd, buffer, sizeof(buffer), 0, (struct sockaddr *) &client_addr, &client_addr_length);
接收資料用的是sys/socket.h
中的recvfrom
函式,第一個引數是Socket Descriptor
,第二個引數是用於儲存資料的buffer
,第三個引數是資料儲存區域的長度(注,對於陣列用sizeof
取到的值是陣列長度乘以長度;對於指標用sizeof
取到的值是指標長度,對於32位機是4,對於64位機是8)。第四個引數是標誌符,一般設定為0,具體可以檢視info recvfrom
。第五個引數用於儲存客戶端的地址,第六個引數是儲存客戶端地址的socklen_t
型變數的長度。
用起來也很好記(也可以現查現用),先是套接字,然後是儲存區及其大小,接著是標誌符,最後是客戶端地址及其大小。
const char *msg_send_2 = "Got your message\n";
sendto(socket_fd, msg_send_2, strlen(msg_send_2), 0, (struct sockaddr *) &client_addr, sizeof(struct sockaddr_in));
傳送資料用更多是sys/socket.h
中的sendto
函式,第一個引數Socket Descriptor
,第二和第三個引數是所傳送的資料及其大小,然後是標示符(一般為0),最後是客戶端地址及其大小。
1.2.1.4 關閉Socket
close(socket_fd);
簡單概括一下 UDP Server 的 Socket 程式設計步驟:
Create Socket - Bind socket with port - Recv/Send - Close
1.2.2 UDP Client
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <netdb.h>
int main(int argc, char *argv[])
{
// Create socket
int socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
// Initialize server address
struct hostent *server_host = gethostbyname(argv[1]);
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET; // socket internet family
bcopy((char *)server_host->h_addr, (char *)&server_addr.sin_addr.s_addr, server_host->h_length); // socket internet address
server_addr.sin_port = htons(atoi(argv[2])); // socket internet port
// Send
char buffer[1024];
sendto(socket_fd, buffer, sizeof(buffer), 0, (const struct sockaddr *) &server_addr, sizeof(struct sockaddr_in));
// Receive and write
struct sockaddr_in client_addr;
socklen_t client_addr_length = sizeof(client_addr);
int msg_recv_length = recvfrom(socket_fd, buffer, sizeof(buffer), 0, (struct sockaddr *) &client_addr, &client_addr_length);
const char *hint = "Got an ACK: ";
write(1, hint, strlen(hint));
write(1, buffer, msg_recv_length);
// Close
close(socket_fd);
return 0;
}
1.2.2.1 建立Socket
int socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
1.2.2.2 傳送和接收資料
sendto(socket_fd, buffer, sizeof(buffer), 0, (const struct sockaddr *) &server_addr, sizeof(struct sockaddr_in));
與 UDP Server 的 sendto 一樣。注意初始化。
1.2.2.3 關閉Socket
close(socket_fd);
總結一下 UDP Client 程式設計的幾個步驟:
Create Socket - Send/Receive - Close
2 圖解 TCP 和 UDP 原理
宣告:圖片來自此處
3 TCP 和 UCP 的 Socket 程式設計對比
3.1 Server
TCP 的過程是:
- Create server socket
- Bind the server socket with client addresses and a local server port
- Listen the server socket
- Accept with blocking, until the connection has established and then get a client socket
- Read and write data from the client socket
- Close the client socket as discontecting
- Close the local server socket
UDP 的過程是:
- Create server socket
- Bind the server socket with client addresses and a local server port
- Receive data and client address through the server socket
- Send data to the client address through the server socket
- Close the local server socket
通過對比,我們可以看到,相同點如下:
- 一開始都要建立 socket
- 接著都要繫結 socket 與本地埠和指定的客戶端地址
- 最後都要關閉本地 socket
不過這些相似點似乎沒什麼價值,還是看看不同點吧。
- TCP 要監聽埠,然後阻塞式地等待連線;UDP 則通過自身的迴圈來不斷讀取,不阻塞也不建立連線。
- TCP 建立連線後會有一個 client socket,然後通過向這個 socket 的讀寫實現資料傳輸;UDP 則直接向客戶端地址傳送和接收資料。
- 因為 TCP 方式有 client socket,所以完成一次傳輸後,可以關閉 client socket。當然也可以一直連著不關閉。
可以看到,TCP 和 UDP 的本質區別就是面向連線還是無連線的。因為面向連線,所以要監聽到是否有 connection 到來,connection 一旦到來,就阻塞住,然後會有一個 socket 跳出來作為代言。通過對這個 socket 的讀寫就實現了對 connection 的另一端的客戶端的讀寫。
3.2 Client
TCP 的過程是:
- Create client socket
- Connect the server address and port with the client socket
- After connection is established, read and write data to the client socket
- Close the local socket socket
UDP 的過程是:
- Create client socket
- Send data to the server address through the client socket
- Receive data and the server address throught the client socket
- Close the local client socket
可以看到如下區別:
- TCP 方式要 connect 伺服器地址/埠與 socket;UDP 則不需要這個過程。
- TCP 方式在 connection 建立後,通過 client socket 讀寫資料;而 UDP 方式則直接通過 client socket 向伺服器地址傳送資料。
3.3 所使用的 API 對比
TCP 方式的 Server 用到:
socket
-
bind
和listen
accept
-
read
和write
close
UDP 方式的 Server 用到:
sokect
bind
-
recvfrom
和sendto
close
TCP 方式的 Client 用到:
socket
connect
-
write
和read
close
UDP 方式的 Client 用到:
socket
-
sendto
和recvfrom
close
4 裸用 socket 的效能很差
是的,這是最傳統的網路程式設計方式:“One traditional way to write network servers is to have the main server block on accept(), waiting for a connection. Once a connection comes in, the server fork()s, the child process handles the connection and the main server is able to service new incoming requests.”
下一篇,我會介紹 IO 複用技術中在 Linux 下常用的 select、poll 和 epoll。
5 參考
- http://www.lowtek.com/sockets/select.html
- http://www.kernel.org/doc/man-pages/online/pages/man7/socket.7.html
- http://www.kernel.org/doc/man-pages/online/pages/man2/listen.2.html
- http://www.kernel.org/doc/man-pages/online/pages/man2/send.2.html
- http://www.kernel.org/doc/man-pages/online/pages/man2/sendto.2.html
- http://www.kernel.org/doc/man-pages/online/pages/man2/recv.2.html
- http://www.kernel.org/doc/man-pages/online/pages/man2/recvfrom.2.html
- http://www.linuxhowtos.org/C_C++/socket.htm
-
Happy Coding, enjoy sharing!
轉載請註明來自“柳大的CSDN部落格”:Blog.CSDN.net/Poechant
-
相關文章
- 網路程式設計基礎程式設計
- Linux系統程式設計基礎Linux程式設計
- Linux網路配置基礎Linux
- Linux網路管理基礎Linux
- JavaScript非同步程式設計-基礎篇JavaScript非同步程式設計
- 驅動篇——核心程式設計基礎程式設計
- JAVA網路程式設計基礎Java程式設計
- Linux基礎之網路管理Linux
- 零基礎學大資料程式設計需要哪些基礎?大資料程式設計
- Java基礎篇--設計模式Java設計模式
- Linux 高效能伺服器程式設計- Linux 網路程式設計基礎 APILinux伺服器程式設計API
- Java 基礎02Java程式設計基礎Java程式設計
- c++程式設計基礎實驗4C++程式設計
- Python基礎程式設計(十六)——函式4Python程式設計函式
- 好程式設計師Java教程分享Java的4大核心基礎程式設計師Java
- 網路程式設計基礎知識程式設計
- 《java程式設計基礎》java的基礎知識(三)Java程式設計
- Linux Shell指令碼程式設計-基礎1Linux指令碼程式設計
- Linux-shell程式設計入門基礎Linux程式設計
- Socket程式設計基礎程式設計
- Go程式設計基礎Go程式設計
- Shell程式設計-基礎程式設計
- python程式設計基礎Python程式設計
- shell程式設計基礎程式設計
- Linux 程式基礎Linux
- MySQL45講基礎篇MySql
- 程式設計必備基礎 計算機組成原理+作業系統+計算機網路,計算機基礎——更適合程式設計師的程式設計必備基礎知識作業系統計算機網路程式設計師
- Python學習之旅(核心程式設計基礎篇6基礎資料型別③)Python程式設計資料型別
- 程式設計基礎四大件程式設計
- Python3.7黑帽程式設計——病毒篇(基礎篇)Python程式設計
- 好程式設計師大資料學習路線分享Scala系列之基礎篇程式設計師大資料
- Linux之19——Shell程式設計基礎詳解Linux程式設計
- Java擴充-網路程式設計基礎Java程式設計
- 【Python入門基礎】網路程式設計Python程式設計
- JAVA SE 實戰篇 C2 網路程式設計基礎Java程式設計
- 【socket程式設計基礎模板】程式設計
- QML程式設計 基礎 小白程式設計
- 【程式設計基礎】輸出程式設計
- 程式設計基礎知識程式設計