第2章 基本的TCP套接字
2.1 IPv4 TCP客戶端
????4個步驟:
(1) socket()創建TCP套接字(window下要用初始化套接字環境)
(2) connect()建立到達服務起的連接
(3) send()和recv() 通信
(4) close關閉連接(Windows 下使用closesock())?
2.1.1 應答(echo)協議的客戶端
程序代碼如下:
文件:practical.h,錯誤處理函數
?
文件:tcp_echo_client.c
#include <stdio.h> /* fputs */ #include <stdlib.h> /* exit, atoi, memset */ #include <unistd.h> /* standard symbolic constants and types and miscellaneous functions: _POSIX_VERSION, _XOPEN_VERSION, R_OK, W_OK, SEEK_SET, SEED_CUR, F_LOCK, STDIN_FILENO, function declarations and so on */ #include <sys/types.h> /* system data types: clock_t, dev_t,gid_t, mode_t, off_t, pid_t, size_t, ssize_t, time_t, uid_t and so on*/ #include <sys/socket.h> /* socket, connect, bind, listen, accept, send, sendto, sendmsg, recv, recvform, recvmsg */ #include <netinet/in.h> /* socket address:sockaddr, sockaddr_in sockaddr_in6 and so on or old system: difine htons, htonl, ntohs, ntohl*/ #include <arpa/inet.h> /*inet_ntop, inet_pton, htons, htonl, ntohs, ntohl */ #include "practical.h" int main(int argc, char *argv[]) {//Test for correct number of argumentsif (argc < 3 || argc > 4) {err_quit("Usage: a.exe <server address> <echo word> [<server port>]");}char *servip = argv[1]; //first arg: server IP address(dotted quad)char *echo_string = argv[2]; //second arg: string to echo//Third arg(optional): server prot(numeric). 7 is well_known echo portin_port_t servport = (argc == 4) ? atoi(argv[3]) : 7;//Create a reliable, stream socket using TCPint sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if (sock < 0) {err_sys("socket failed");}//Construct the server address structurestruct sockaddr_in servaddr; //IPv4 server addressmemset(&servaddr, 0, sizeof(servaddr));servaddr.sin_family = AF_INET; //IPv4 address family//Convert addressint rtnval = inet_pton(AF_INET, servip, &servaddr.sin_addr.s_addr);if (rtnval == 0) {err_quit("inet_pton failed: invalid address string");} else if (rtnval < 0) {err_sys("inet_pton failed");}servaddr.sin_port = htons(servport); //server port//Establish the connection to the echo serverif (connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) {err_sys("connect failed");}size_t echo_stringlen = strlen(echo_string);//Send the string to the serverssize_t numbytes = send(sock, echo_string, echo_stringlen, 0);if (numbytes < 0) {err_sys("send() failed");} else if (numbytes != echo_stringlen) {err_quit("send(), send unexpected of bytes");}//Receive the same string back from the serverunsigned int totalbytesrecvd = 0; //count of total bytes receivedfputs("Received: ", stdout);while (totalbytesrecvd < echo_stringlen) {char buf[BUFSIZ]; //I/O buffer//Receive up to the buffer size (minus 1 to leave space for a//null terminator) bytes from the sendernumbytes = recv(sock, buf, BUFSIZ - 1, 0);if (numbytes < 0) {err_sys("recv() failed");} else if (numbytes == 0){err_quit("connection closed prematurely");}totalbytesrecvd += numbytes;buf[numbytes] = '\0';fputs(buf, stdout);}fputs("\n", stdout); close(sock);exit(0); }????注意:TCP是一種字節流協議,它的一種實現是不會保持send()邊界。通過在連接的一端調用send()發送的字節可能不會通過在另一端單獨調用一次recv而全部返回,需要反復接受。?
????編寫套接字應用程序基本原則:對于另一端的網絡和程序將要做什么事情,永遠不要做出假設。
????在給用戶提示信息是,若需要格式化則使用printf(),否則使用fputs()。應該避免使用printf輸出固定的,預先格式化的字符串。你從來都不應該把從網絡收到的文本作為第一個參數傳遞給printf(),這會引起嚴重的安全問題,要用fputs()。?
2.2 IPv4 TCP服務器
????服務器職責是建立通信端點,被動等待客戶的連接。
????(1)socket()創建TCP套接字
????(2)利用bind()給套接字分配端口
????(3)listen()告訴系統允許對端口建立連接
????(4)反復執行以下操作:
????????a.調用accept為每個客戶連接獲取新的套接字
????????b.使用send()和recv()通過新的套接字與客戶通信
????????c.使用close()關閉客戶連接。
程序代碼如下:
文件:tcp_echo_server.c
?
運行時:windows?下建議安裝Cygwin,配置vim
????gcc practical.c tcp_echo_client.c -o client?????? 生成client
????gcc practical.c tcp_echo_server -o server????????生成server
先運行:server.out?端口號(如:5000)
client IPv4地址? 顯示字符串? 端口號(5000)。
????與客戶端的不同:服務器中套接字的使用必須涉及一個地方綁定到套接字,然后把該套接字用作獲得連接到客戶的其他套接字的方式??蛻舯仨毥oconnect提供服務器的地址,而服務器必須給bind()指定它自己的地址。他們要對這份信息(服務器的地址和端口)達成協議進行通信,它們實際上都不需要知道客戶的地址。
????編寫套接字網絡應用程序的關鍵是:防御型編程,你的代碼絕對不能對通過網絡接受到的任何信息做出假設。
????可以使用telnet 連接到服務器,telnet?IP地址。
2.3 創建和銷毀套接字
????套接字是通信端點的抽象,訪問套接字需要套接字描述符,在unix下是用文件描述符實現的。許多處理文件描述符的函數(read, write等)可以處理套接字描述符。
????#include?<sys/socket.h>
????int socket(int domain, int type, int protocol)
????????????????????????????返回值:成功返回文件(套接字)描述符,失敗返回-1
用于創建套接字的實例。
????domain:套接字的通信領域,
????????????????AF_INET????????????IPv4
????????????????AF_INET6? ????????IPv6
????????????????AF_UNIX?? ????????UNIX域(AF_LOCAL域)
????????????????AF_UNSPEC?????任何域
????type:套接字類型,進一步確定通信特征
????????????????SOCK_DGRAM?? ? ? ?長度固定的,無連接的不可靠報文傳遞,UDP默認協議
????????????????SOCK_RAW ? ? ? ? ? ?IP協議的數據報接口(POSIX.1中可選)
????????????????SOCK_SEQPACKET??長度固定有序,可靠的面向連接報文傳遞
????????????????SOCK_STREAM?? ? ? 有序,可靠,雙向的面向連接字節流,TCP默認協議
????protocol:端到端協議,通常為0,代表默認。SOCK_SEQPACKAGE與SOCK_STREAM類似,但從套接字得到的是基于報文服務的而不是字節流服務。這意味著它的套接字接收的數據量與對方發送的一致。流控制傳輸協議(Stream Control Transmission Protocol, SCTP)提供順序數據報服務。SOCK_RAW套接字提供一個數據報接口用于直接訪問下面的網絡層(IP層)。使用這個接口時,應用程序負責構造自己的協議首部,傳輸協議(TCP, UDP)被繞過。當創建一個原始套接字時需要超級用戶特權,用以防止惡意程序繞過內建安全機制來創建報文。
????調用socket類似于open,在兩種情況下均可以獲得用于輸入輸出的文件描述符。不需要描述符時,調用close,釋放該描述符以便重新使用,套接字描述符本質上一個文件描述符,但不是所有參數為文件的函數都可以接受套接字描述符。
????關閉套接字
????#include <sys/socket.h>
????int close(int socket);
函數告訴底層協議棧發起關閉通信以及釋放與套接字關聯的資源。
?????套接字關閉是雙向的,使用
????#include <sys/socket.h>
????int shutdown(int sockfd, int how);?
?????????????????????????????????返回值:成功0,失敗-1
禁止套接字上的輸入輸出。
????how: SHUT_RD,關閉讀端,無法從套接字讀取數據。
? ? ? ? ? ? SHUT_WR?,關閉寫端,無法從套接字發送數據。
? ? ? ? ? ? SHUT_RDWR,?關閉讀寫。
2.4 指定地址
????要確定通信目標進程,使用網絡地址確定想要與之通信的計算機,服務(端口號)標識計算機上特定進程。
????字節序列:大端法,低位地址標識高位字節(類似于我們寫的數字,地址由低到高,左邊為高位)。小端法相反,低位地址表示低位字節。(inter平臺,小端,Power PC,SUN大端)
????網絡字節序列使用大端法,因此需要處理器字節序列與網絡字節序列的轉換。
????#include??<arpa/inet.h>????????????????//某些老系統<netinet/in.h>
????uint32_t????htonl(uint32_t? hostint32);????????//主機字節序列轉換到網絡字節序列long int(32位)
????????????????????????????????????????????返回值:以網絡字節序列表示的32位整形
????uint16_t????htons(uint16_t? hostint16);????????//主機字節序列轉換到網絡字節序列short int(16位),常用于轉換Port號
????????????????????????????????????????????返回值:以網絡字節序列表示的16位整形
????uint32_t????ntohl(uint32_t? hostint32);????????//網絡字節序列long int(32位)轉換到?主機字節序列?
?????????????????????????????????????????? ?返回值:以主機字節序列表示的32位整形
????uint16_t????ntohl(uint16_t? hostint16);???????//網絡字節序列long short(16位)轉換到?主機字節序列???????????????????????????????????????????
????????????????????????????????????????????返回值:以主機字節序列表示的16位整形
2.4.1?通用地址
????以一個泛型地址用于傳遞給套接字函數。其它地址必須強制轉換為它。
????#include <netinet/in.h>????????????//定義了所有地址
????struct?? socketaddr? {
????????sa_family_t????sa_family;????????????//Address family (e.g., AF_INET(6))
????????char????????????????sa_data[];????????????//variable-address length
????}
????套接字實現可以自由的添加額為的成員和定義sa_data[]的大小。
????在Linux下,
????struct?? socketaddr? {
????????sa_family_t????sa_family;
????????char????????????????sa_data[14];
????}????
????FreeBSD下,
????struct?? socketaddr? {
????????unsigned? int?? sa_len;????????????????//total? length
????????sa_family_t????sa_family;
????????char????????????????sa_data[14];
????}????
2.4.2?? IPv4地址
????sockaddr的結構依賴于IP版本。
????struct?? in_addr {
????????in_addr_t? s_addr;????????????????//IP address
????}
????
????struct? sockaddr_in{
????????sa_family_t??? sin_family;????????????????//internet? procotol? (AF_INET)
????????in_port_t????????sin_port;????????????????????//address port????(16 bits)
????????struct????in_addr????sin_addr;????????????//IPv4 address (32 bits)
????????char???? sin_zero[8?];????????????????????????//not used
??? }
????in_port_t?是uint16_t, in_addr_t是uint32_t,都被定義在<stdint.h>中
????sockaddr_in只是sockaddr結構的更詳細視圖,是為IPv4套接字定制的。把sockaddr_in強制轉換為sockaddr時,套接字函數會檢測sa_familiy字段獲取實際類型,然后強制轉換為合適類型。
2.4.3 IPv6地址
????struct? in6_addr {
????????uint8_t? s_addr[16];
????}
????struct? sockaddr_in6 {
????????sa_family_t? sin6_family;????????????//AF_INET6
????????in_port_t? ????sin6_port;
????????uint32_t????????sin6_flowinfo;????????//traffic class and flow? information
????????struct? in6_addr sin6_addr;????????//IPv6 address (128 bits)
????????uint32_t????????sin6_scope_id;??????//set of interface of scope
??? }
2.4.4?通用地址存儲器
????sockaddr不足以存放sockaddr_in6,在我們想為一個地址結構分配大小時,但是不知道實際類型(4或6),sockaddr不工作,因為它對與某些結構大小。使用sockaddr_storage結構,保證與任何支持的地址類型一樣大。
struct????sockaddr_storage? {
????sa_family_t????????????????????//前導地址組字段,確定地址的實際類型
????...
}
????在一些系統,地址結構包含一個額外的存儲地址結構長度(字節為單位)的字段。由于長度字段并非所有系統都可用,應該避免使用,通常系統會提供一個值用于測試長度是否存在。
2.4.5二進制/字符串地址轉換
????套接字函數只能理解數字(二進制形式),但是人們使用的是“可打印字符串(如:192.178.2.2,1::1)”。
????#include <arpa/inet.h>
????const? char?*inet_ntop(int domain, const void *restrict addr, char *restrict str, socket_t size);
????????????????????????返回值:成功返回地址字符串指針,出錯返回NULL(將數字(二進制網絡地址)轉換為23.24.54.4類型地址)
????int inet_pton(int domain, const char *restrict str, void *restrict addr);
?????????????????????????返回值:成功返回1,格式無效返回0,出錯返回-1(pton=printable to numeric)(將23.3.4.5類型地址轉換為數字,網絡地址(二進制))
????domain:地址族,只有AF_INET或AF_INET6。
??? 對于inet_ntop(), size:保存文本緩存區(可打印的地址str)的大小。兩個常數用于簡化工作,INET_ADDRSTRLEN定義足夠大的空間(可能最長的結構字符串)存放IPV4地址。INET6_ADDRSTRLEN定義用于存放IPv6的空間。
????對于inet_pton(),str是一個null結尾的字符串,addr要足夠大,IPv4至少32為,IPv6至少128位。
2.5將套接字與地址綁定
???與客戶端關聯的套接字地址沒有太大意義,可以讓系統選擇一個默認的地址。對于服務器需要給接收客戶端請求的套接字綁定一個眾所周知地址。客戶端可以為服務器保留一個地址并在/etc/services或在某個名字服務(name service)中注冊。
????#include <sys/socket.h>
????int bind(int sockfd, struct sockaddr *localaddress, socket_t len);?????
????????????????????????返回值:成功返回0,出錯返回-1
????sockaddr設置為通配符INADDR_ANY(IPv4),IN6ADDR_ANY(IPV6),這是套接字可綁定到所有的系統網絡接口,意味著可以接收系統所有網卡的數據報。調用connect和listen時沒有綁定到一個套接字,系統會選擇一個地址綁定到套接字。
????可以使用IN6ADDR_ANY_INIT把in6_addr結構初始化為通配符地址,但是這個常量只能用于聲明中的“初始化器”。注意:inaddr_any是主機字節序列,在用作bind()的參數之前必須先轉換為網絡字節序列。in6addr_any和in6addr_any_init已經是網絡字節序列。
????當端口號為0提供給bind(),系統將為你選擇未使用的本地端口。
2.6.獲取套接字的關聯地址
????獲取套接字關聯的本地地址
????#include?<sys/socket.h>
??? int? getsockname(int sockfd, struct sockaddr *localaddress, socket_t *addresslength);
????????????????????返回值:成功放回0,出錯返回-1
獲取套接字關聯的外部地址,套接字已經和對方連接,用于獲取對方的地址
????#include <sys/socket.h>
????int getpeername(int sockfd, struct sockaddr *remoteaddress, socket_t *addresslength);
????????????????????????????返回值:成功放回0,出錯返回-1
????sockfd: 想要獲取其地址的套接字描述符。
????remoteaddress和localaddress指向實現把地址信息存放在其中的地址結構,總會被強制轉換為sockaddr*。若事先不知道IP協議版本,可以傳入一個sockaddr_storage*接受結果。
????addresslength:輸入/輸出型參數,是指向整形的指針(輸入),整形指定sockaddr的大小,返回時(輸出)被設置成返回地址的大小。若該地址與緩沖區長度不匹配,將其截斷不報錯。若沒有綁定到套接字的地址,結果無意義。
2.7建立連接
????處理面向連接的網絡服務(SOCK_STREAM或SOCK_SEQPACKAGE),需要客戶端套接字與服務區端套接字進行連接。
????#include?<sys/socket.h>
????int connect(int sockfd, const struct sockaddr *foreignaddress, socklen_t addresslen);
????????????????????????返回值:成功返回0,出錯返回-1
connect中的地址foreignaddress是想要與之通信的地址,如果套接字沒有綁定到一個地址,connect會給調用者綁定一個默認接口。
addresslength指定地址結構的長度,通常給出sizeof(struct sockaddr_in)或sizeof(struct sockaddr_in6)。
2.8處理進入的連接
?綁定后服務器套接字就具有一個地址(或至少一個套接字)。指示底層實現偵聽來自套接字
的連接使用listen:
?#include <sys/socket.h>
?int listen(int socket, int queuelimit);
????返回值:成功0,失敗-1
queuelimit:任意時間等待進入連接數量的上限。實際值由系統指定,上線由<sys/socket.h>
中SOMAXCONN指定。一旦隊列滿了就拒絕處理連接的請求。
?listen()導致內部狀態改變為給定的套接字,使得將會處理進入的TCP連接請求。然后對
它們進行排隊。
? ? ?一旦把套接字配置為偵聽,就可以開始接受其上客戶的連接。首先,服務器現在似乎應該等待
它設置的套接字上的連接,通過套接字進行發送和接收,關閉它,然后重復這個過程,但是實際
上不是這樣。已經被綁定到端口并且標記為“偵聽”的套接字實際上從來不會用于發送和接收
。它被代之用作獲取新套接字的方式,其中每個新套接字用于一條客戶連接,服務器然后在
新套接字上執行發送和接收。
?調用accept獲取用于連接的套接字:
?#include <sys/socket.h>
?int accept(int sockfd, struct sockaddr *clientaddress, socket_t *addresslength);
????返回值:成功返回文件(套接字)描述符,失敗返回-1
? ? 返回的套接字連接到connect的客戶端。這個新的套接字描述符和原始套接字(sockfd)具有相同的套接字
類型和地址族。傳給accept的原始套接字沒有關聯到這個連接,而是繼續保持原來狀態并接受其它連接
請求。
? ? 這個函數為套接字使隊列的下一條連接出隊,如果隊列為空,就阻塞,直到下一個連接請求到達。
成功時就會利用連接到另一端的客戶的地址和端口填充由clientaddr指定的結構的大小(即可用的空間
),一旦返回,就會包含反回的實際地址的大小。成功,返回連接到客戶的新套接字的描述符。一旦失敗
返回-1,大多數系統僅當傳遞了一個錯誤的套接字描述符時,accept才會失敗。在一些平臺,如果套接字
在創建后并在被接受前經歷了網絡級錯誤,那么它可能返回一個錯誤。?
? ? 若不關心客戶端標識,可以將參數addr和len設為NULL,否則在accept之前,應將參數clientaddr設為足夠大的?緩沖區
來存放地址,并將addresslength設為設為代表這個緩沖區大小的整數的指針。返回時,accept會在緩沖區
填寫客戶端的地址并且更新指針addresslength所指向的整數為該地址大小。
? ? ?如果沒有連接請求,就阻塞到一個請求到來,如果sockfd是非阻塞模式,accept返回-1并將error設為
EAGAIN或EWOULDBLOCK(很多平臺,EAGIN定義于EWOULDBLOCK相同)。
?
?2.9 數據傳輸
?在連接后,服務器和客戶端的區別就消失了(至少socket api是這樣)。連接的TCP套接字可以使用
?#inlcude <sys/socket.h>
?ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags);
???返回值:返回發送的字節數,出錯返回-1
發送數據。它與write類似,但是可以指定標志來改變處理傳輸數據的方式。使用send時套接字必須已經連接。
參數buf和nbytes和write中相同。
?flags:
?MSG_DONTROUTE:勿將數據路由出本地網絡。
?MSG_DONTWAIT: 允許非阻塞操作(等價于O_NONBLOCK)
?MSG_EOR:????? 如果協議支持,此為記錄結束。
?MSG_OOB:??如果協議支持,發送外帶數據。
? ? ?send成功返回,并不必然表示連接另一端的進程接收數據,只表示數據已經無錯誤的發送到網絡。
對于支持為報文設限的協議,如果單個報文超過協議所支持的最大尺寸,send失敗并將error設為EMSGSIXZE,
對于字節流協議,send會默認會阻塞直到整個數據被傳輸。
?#include <sys/socket.h>
?ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags);
???返回值:以字節計數的消息長度,若無可用消息或對方已經按序結束則返回0,出錯返回-1
?函數recv與read類似,但允許指定選項控制如何接受。
?flag
?MSG_OOB:??? 如果協議支持,接受外帶數據
?MSG_PEEK:?返回報文內容而不真正取走報文
?MSG_TRUNC:?即使報文被截斷,要求返回的是報文的實際長度
?MSG_WAITALL:等待直到所有的數據可用(僅SOCK_STREAM)
?
?當指定MSG_PEEK標志時,可以查看寫一個要讀的數據而不真正取走,再次調用read或recv函數會返回
剛才查看的數據。
對于SOCK_STREAM套接字,接受的數據可以比請求的少,MSG_WAITALL阻止這種行為,除非需要的數據
全部接受recv才返回。對于sock_DGRAM和SOCK_SEQPACKET套接字,它不改變什么行為,因為基于報文的套接字
類型一次讀取就返回整個報文。
?如果發送者已經調用shutdown結束傳輸,或者網絡協議支持默認的順序關閉并且發送端已經關閉,那么
當所有的數據接受完畢后,recv返回0。
?buf指向緩沖區,其中存放接收到的數據,len為緩沖區的長度,它是一次可以接受的最大字節數。recv默認
行為是阻塞到至少傳輸一些字節位置(在多數系統上,將導致recv的通用者解除阻塞的最小數據量是1字節)。
?注意:TCP是一種字節流協議,不會保留send()邊界。在接收者上調用recv()一次所讀取的字節數不一定
由調用send()一次所寫入的字節數確定。
?
?2.10使用IPv6
?IPv6程序涉及了IPv6的地址結構和常量,其它沒有什么不同。
?socket(AF_INTE6, SOCK_STREAM, IPPROTO_TCP);
?struct sockaddr_in6 servaddr;
?memset(&servaddr, 0, sizeof(servaddr));
?servaddr.sin6_family = AF_INET6;
?servaddr.sin6_addr?? = in6addr_any;??//不需要轉換為網絡字節序列,已經是了。
?servaddr.sin6_port?? = htons(servport);
?IPv6的地址長度:INET6_ADDRSTRLE;
轉載于:https://www.cnblogs.com/hancm/p/3668355.html
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的第2章 基本的TCP套接字的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 为何jsp 在resin下乱码,但在to
- 下一篇: Cracking The Coding