近期學完TCP/IP協議,東拼西湊寫了一個簡單Socket程序。在此總結一下,希望總結完成之后能領悟一些東西。
1.什么是Socket?
要了解這個問題首先來看一張圖,
其實Socket,就是一組函數,它們和Unix I/O 函數結合起來,用以創建網絡應用。由圖可以看出Socket介於應用層和運輸層之間,是一組接口。它把復雜的TCP/IP協議族隱藏在Socket接口中,給用戶看到的只有一組接口。
2.進程之間的通信
學完TCP/IP協議都知道,從IP層來說,通信的兩端是兩台主機,但是實際上真正進行通信的實體是在主機中的進程,是這台主機的一個進程和另一個主機的一個進程之間交換數據,端到端的通信其實是應用進程之間的通信。在網絡層,IP地址唯一標識一台主機,而運輸層中的"協議+端口"可以唯一標識一個主機中應用進程,因此"IP地址+協議+端口"其實就可以唯一標識網絡中的一個應用進程了。
3.Socket的一些基本操作
3.1 socket()函數
定義:
int socket (int domain, int type, int protocol); // 成功返回描述符, 出錯返回-1
- domain是地址族常用的有AF_INET和AF_INET6分別代表IPv4和IPv6的地址。
- type為數據傳輸方式/套接字類型,常用的有SOCK_STREAM(流格式套接字/面向連接的套接字和SOCK_DGRAM(數據報套接字/無連接的套接字。這里又是另一塊內容,簡言之,TCP使用的是SOCK_STREAM,UDP使用的是SOCK_DGRAM。
- protocol表示傳輸協議,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分別表示 TCP 傳輸協議和 UDP 傳輸協議。
注意:當protocol為0時,會自動選擇type類型對應的默認協議。
socket函數對應於普通文件的打開操作。普通文件的打開操作返回一個文件描述字,而socket()用於創建一個socket描述符(socket descriptor),它唯一標識一個socket。這個socket描述字跟文件描述字一樣,我的理解一個socket描述字就像一個特定文件一樣,把它作為參數,通過它來進行一些讀寫操作,我認為type就相當於指定了文件讀寫的方法一樣。
3.2 bind()函數
定義:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // 成功返回0,出錯返回-1
- sockfd指的就是socket()函數調用之后返回的那個socket描述符,唯一標識一個描述符。
- addr指針指的是要綁定給sockfd的的協議地址。IPv4對應的地址結構有兩種。分別是:
struct sockaddr_in {
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
struct in_addr {
union {
struct { Uunsigned char s_b1,s_b2,s_b3,s_b4; } S_un_b;
struct { unsigned short s_w1,s_w2; } S_un_w;
unsigned long S_addr;
} S_un;
};
struct sockaddr {
u_short sa_family;
char sa_data[14];
};
這兩種地址有何不同呢,通過觀察可以發現,這兩個結構都是16字節的存儲空間(因此很容易由sockaddr_in 轉換成為sockaddr),但是sockaddr_in中有端口可以由程序員指定,而sockaddr中並沒有可以指定端口的定義,因此建議程序員使用sockaddr_in,在需要使用sockaddr時將sockaddr_in轉換為sockaddr。
3. addrlen地址的長度(這里要留個坑,是一個細節)。
bind()函數的作用是什么呢?通常服務器在啟動的時候都會綁定一個眾所周知的地址(如ip地址+端口號),用於提供服務,客戶就可以通過它來接連服務器;而客戶端就不用指定,有系統自動分配一個端口號和自身的ip地址組合。這就是為什么通常服務器端在listen之前會調用bind(),而客戶端就不會調用,而是在connect()時由系統隨機生成一個。
3.3 listen()函數(僅由TCP服務器調用)
定義:
int listen(int sockfd, int backlog); // 成功返回0, 出錯返回-1
1.sockfd套接字,表明被監聽的套接字描述符。
2.backlog,表明相應隊列要排隊的未完成請求的數量。(這里先留個坑,隨后補上)
那么listen是怎么工作的呢?
listen把sockfd從主動套接字轉化為一個被動套接字(使用socket()函數,默認生成一個主動套接字),使套接字可以接受來自客戶端的請求。
3.4 accept()函數(僅由TCP服務器調用)
定義:
int accept(int listenfd, struct sockaddr*addr, int *addrlen); // 成功返回連接描述符, 出錯返回-1
- listenfd,被監聽的套接字才能調用accept()。
- addr,客戶端的addr,是被填寫的addr,因此使用前要先創建一個空addr。
- addrlen,一個指向地址長度的指針,是被寫的。
注意看注釋,返回的是一個已連接描述符,並不是客戶端的描述符,這里要畫重點了,下面說connect()的時候會詳細說明。
accept的作用?accept函數等待來自客戶端的連接請求到達被動描述符,然后在addr中填寫客戶端的套接字地址,並返回一個連接描述符。
3.4 connect()函數(由客戶端發起請求)
定義:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // 成功返回0, 出錯返回-1
這個函數是由客戶端發起的,其實不用詳細說明,根據前面的解釋,用於連接服務器和accept()建立聯系的函數。
這里要強調的有一點,就是和accept()之間建立聯系的過程,這里由一張圖給出。
connfd就是accept()返回的連接套接字。
3.5 read()和write()等函數
接下來是讀寫函數了,有很多組,這里只介紹兩組分別是
recv()和send()
int recv(int sock, char *buf, int len, int flags); // 成功返回讀取的字節數,失敗返回-1
int send( int sock, const char *buf, int len, int flags ); // 成功返回發送字節數,失敗返回-1
- sock指定接收端套接字描述符(即連接描述符)
- buf指明一個緩沖區,該緩沖區用來存放recv函數接收到的數據
- len指明buf的長度
- flags一般置0
recvfrom()和sendto()
int recvfrom(int sock, const char *buf, int len, int flags, const struct sockaddr* from, int* fromlen) // 成功則返回接收到的字節數,失敗返回-1
int sendto(int sock, const char *buf, int len, int flags, const struct sockaddr* to, int tolen) // 成功返回發送的字節數,失敗返回-1
前四個參數和recv一樣,這里不做贅述了,recvfrom會從接受端套接字接收數據並且會得到發送方的套接字地址,而sendto會從發送端發送數據給需要接收的地址。
這兩組函數對於TCP和UDP來說其實都是可用的,UDP也可以connect之后使用recv和send,(又給自己挖了個坑,以后有時間補上)。
接下來是TCP版完整程序,首先是TCP的接發數據的流程,如圖
#include <winsock.h>
#pragma comment(lib,"WS2_32") // 鏈接到WS2_32.lib
#include <stdlib.h>
// 用來初始化winsock
class InitSock
{
public:
InitSock(BYTE minorVer = 2, BYTE majorVer = 2)
{
// 初始化WS2_32.dll
WSADATA wsaData;
WORD sockVersion = MAKEWORD(minorVer, majorVer);
if (::WSAStartup(sockVersion, &wsaData) != 0) // 返回0表示正常情況,否則退出程序
{
exit(0);
}
}
~InitSock()
{
::WSACleanup();
}
};
使用winsock時必須要初始化,這段代碼將初始化封裝在一個類里面,程序開始前創建一個對象就行了。(關於#pragma comment(lib,"WS2_32") 在這里留個坑,以后有空再說)
TCPServer.cpp
#include "InitSock.h"
#include <iostream>
#include <string>
InitSock sock;
int main() {
SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sListen == INVALID_SOCKET) {
std::cout << "Failed socket()" << std::endl;
return -1;
}
sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(4567);
sin.sin_addr.S_un.S_addr = INADDR_ANY; // inet_addr("0.0.0.0");
// 綁定這個套接字到一個本地地址
if (::bind(sListen, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR) {
std::cout << "Failed bind()" << std::endl;
return -1;
}
// 進入監聽模式
if (::listen(sListen, 2) == SOCKET_ERROR)
{
printf("Failed listen() \n");
return -1;
}
// 循環接受客戶的連接請求
sockaddr_in remoteAddr;
int nAddrLen = sizeof(remoteAddr);
SOCKET sClient = 0;
char szText[] = " TCP Server Demo! \r\n";
while (sClient == 0)
{
// 接受一個新連接
sClient = ::accept(sListen, (SOCKADDR*)&remoteAddr, &nAddrLen); // 會阻塞等待直到連接成功后返回一個套接字
if (sClient == INVALID_SOCKET)
{
printf("Failed accept()");
}
std::cout << "接收到一個連接:" << inet_ntoa(remoteAddr.sin_addr) << std::endl;
continue;
}
while (TRUE)
{
// 從客戶端接收數據
char buff[256];
int nRecv = ::recv(sClient, buff, 256, 0);
if (nRecv > 0)
{
buff[nRecv] = '\0';
std::cout << "接收到的數據:" << buff << std::endl;
}
// 向客戶端發送數據
std::cin >> szText;
::send(sClient, szText, strlen(szText), 0);
}
// 關閉同客戶端的連接
::closesocket(sClient);
// 關閉監聽套節字
::closesocket(sListen);
return 0;
}
TCPClient.cpp
#include <iostream>
#include "InitSock.h"
#include <string>
InitSock sock; // 初始化winsock 庫
int main()
{
SOCKET s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // 創建套接字
if (s == INVALID_SOCKET) {
std::cout << "Failed socket()" << std::endl;
return -1;
}
// 也可以在這里調用bind函數綁定一個本地地址
// 否則系統將會自動安排
sockaddr_in addr; // 遠程地址
addr.sin_family = AF_INET;
// 填寫的是服務器程序的ip地址
addr.sin_port = htons(4567); // 用來保存端口號
addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
if (::connect(s, (sockaddr*)&addr, sizeof(addr)) == -1) {
std::cout << "Failed connect()" << std::endl;
return -1;
}
char buff[256];
char text[256];
while (true) {
// 向服務端發送數據
std::cin >> text;
::send(s, text, strlen(text), 0);
int nrecv = ::recv(s, buff, 256, 0);
if (nrecv > 0) {
buff[nrecv] = '\0';
std::cout << "接收到數據:" << buff << std::endl;
}
}
::closesocket(s);
return 0;
}
接下來時UDP版的:
UDPServer.cpp
#include <iostream>
#include "InitSock.h"
InitSock sock;
int main() {
SOCKET sock_Server = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sock_Server == INVALID_SOCKET)
{
std::cout << "Failed socket()" << std::endl;
return -1;
}
sockaddr_in ser_addr;
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(8888);
ser_addr.sin_addr.S_un.S_addr = INADDR_ANY; // "0.0.0.0"
// UDP不需要監聽
if (::bind(sock_Server, (LPSOCKADDR)&ser_addr, sizeof(ser_addr)) == SOCKET_ERROR) {
std::cout << "Failed bind()" << std::endl;
return -1;
}
char sendmsg[256];
char recvmsg[256];
sockaddr_in cli_addr;
int len = sizeof(cli_addr);
while (TRUE) {
int count = recvfrom(sock_Server, recvmsg, 256, 0, (sockaddr*)&cli_addr, &len);
if (count == -1) {
std::cout << "Recive data fail!" << std::endl;
return -1;
}
std::cout << "收到的數據:" << recvmsg << std::endl; // 打印收到的數據
std::cin >> sendmsg;
sendto(sock_Server, sendmsg, 256, 0, (sockaddr*)&cli_addr, len);
}
::closesocket(sock_Server);
return 0;
}
UDPClient.cpp
#include <iostream>
#include "InitSock.h"
#include <string>
InitSock sock; // 初始化winsock 庫
int main()
{
SOCKET s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // 創建套接字
if (s == INVALID_SOCKET) {
std::cout << "Failed socket()" << std::endl;
return -1;
}
// 也可以在這里調用bind函數綁定一個本地地址
// 否則系統將會自動安排
sockaddr_in addr; // 遠程地址
addr.sin_family = AF_INET;
// 填寫的是服務器程序的ip地址
addr.sin_port = htons(4567); // 用來保存端口號
addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
if (::connect(s, (sockaddr*)&addr, sizeof(addr)) == -1) {
std::cout << "Failed connect()" << std::endl;
return -1;
}
char buff[256];
char text[256];
while (true) {
// 向服務端發送數據
std::cin >> text;
::send(s, text, strlen(text), 0);
int nrecv = ::recv(s, buff, 256, 0);
if (nrecv > 0) {
buff[nrecv] = '\0';
std::cout << "接收到數據:" << buff << std::endl;
}
}
::closesocket(s);
return 0;
}