滿山的紅葉……飄落之時……
最近接觸了點關於用C++寫socket的東西,這里總結下。
這里主要是關於TCP的,TCP的特點什么的相關介紹在我另一篇博文里,所以這里直接動手吧。
我們先在windows下寫,不過代碼可以直接移植到linux下。
Visual Studio項目配置及初始化
這里用的版本是2015的。創建了項目之后要配置項目的屬性:

在下圖箭頭處添加ws2_32.lib,不然沒辦法使用socket相關的函數。

然后在win平台下,使用這個庫前需要初始化,因此在main函數中應有:
WSADATA ws;
WSAStartup(MAKEWORD(2, 2), &ws);
不過因為只是在win平台下才需要編譯,所以可以這樣寫:
#ifdef WIN32
static bool first = true;
if (first) {
WSADATA ws;
WSAStartup(MAKEWORD(2, 2), &ws);
first = false;
}
#endif
原理是,在win32平台下編譯的時候,宏WIN32是有定義的,所以會自動執行ifdef到endif之間的代碼,初始化庫。如果是Linux平台下的話則不會執行這段代碼。不過前提是,使用的編譯平台為:

x64的話宏是另一個。
上面代碼中的first是為了保證只初始化一次,雖然多次初始化不會有問題,但是會對性能有一定的影響。
雖然可以直接寫在main函數中初始化,但是為了后面拓展方便,最好封裝在類中。類的構造函數如下:
XTcp::XTcp()
{
// 初始化庫,如果不初始化的話會直接導致后面的socket函數無法使用,但是在初始化前
// 要加載Windows的網絡庫,就是在項目屬性那里加ws2_32.lib
#ifdef WIN32
static bool first = true;
if (first) {
WSADATA ws;
WSAStartup(MAKEWORD(2, 2), &ws);
first = false;
}
#endif
}
這里說一下類的頭文件/聲明:
#ifndef XTCP_H
#define XTCP_H
#ifdef WIN32
#ifdef XSOCKET_EXPORTS
#define XSOCKET_API __declspec(dllexport)
#else
#define XSOCKET_API __declspec(dllimport)
#endif
#else
#define XSOCKET_API
#endif
//#include <string>
class XSOCKET_API XTcp
{
public:
int CreateSocket();
bool Bind(unsigned short port);
XTcp Accept();
void Close();
int Recv(char* buf, int bufsize);
int Send(const char* buf, int sendsize);
bool Connect(const char *ip, unsigned short port, unsigned int timeoutms=1000);
bool SetBlock(bool isblock);
XTcp();
virtual ~XTcp();
unsigned short port = 0; // 用來建立連接的端口
int sock = 0; // 用來通信的socket
char ip[16];
};
#endif
注意,因為是雙方都可以收發,所以必須是雙方都有一個用來接收的函數,一個發送的函數。其實這里寫的服務端代碼和客戶端代碼是一樣的,如果讀者有興趣的話再自行拓展。
配置服務端
在配置之前先弄清楚大概是怎么個流程。首先我們會監聽一個端口,這個端口只是用來接收請求然后建立連接的,但是不會用來傳輸數據。客戶端請求之后服務器會另外分配一個端口,客戶端和服務端是通過這個新分配的端口來進行通信的。
監聽指定端口
了解了大概的流程之后我們就可以開始編寫了,首先是監聽和建立連接的部分:
bool XTcp::Bind(unsigned short port) {
if (sock <= 0) {
CreateSocket();
}
sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(port); // host to network,本地字節序轉換成網絡字節序
saddr.sin_addr.s_addr = htons(0); // 綁定ip地址,0的話其實可以不轉。這里是任意的ip發過來的數據都接受的意思。至於為什么0就是監聽任意端口,建議看看計算機網絡
// 一個int是4個char,所以可以通過int來表示ip地址
// bind端口,很容易失敗,一定要有判斷
if (::bind(sock, (sockaddr*)&saddr, sizeof(saddr)) != 0) { // :: 表示用的是全局的函數
printf("bind port %d failed!", port);
return false;
}
printf("bind port %d succeeded.", port);
listen(sock, 10); // 監聽指定的端口,只用來創建鏈接
return true;
}
上面這段代碼很簡單(都有注釋了欸!),就是先指定一個端口用來建立連接(就是代碼里面所謂的“綁定”),監聽這個端口,一有請求就創建連接。注意::bind,不要省略掉冒號,這里代表使用全局的bind,而不是c++自帶的bind。使用這個函數的時候給個端口號就可以綁定了。
上面代碼用到的CreateSocket()函數的定義如下:
int XTcp::CreateSocket() {
// 使用TCP/IP協議,所以AF_INET,TCP,所以是SOCK_STREAM
sock = socket(AF_INET, SOCK_STREAM, 0);
// 創建socket失敗,例如Linux中因為超出了每個進程分配的文件具體數量而被拒絕創建
if (sock == -1) {
printf("Create socket failed!\n");
}
return sock;
}
其實就是配置一下socket屬性,不解釋。注意這是在類里面操作的,操作的sock是類的屬性。
發送連接請求
發送連接請求要知道ip地址和端口號,這里封裝好了,只需要提供端口號、ip地址、超時時間即可。
bool XTcp::Connect(const char * ip, unsigned short port, unsigned int timeoutms)
{
if (sock <= 0) {
CreateSocket();
}
sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(port);
saddr.sin_addr.s_addr = inet_addr(ip);
SetBlock(false);
fd_set set; // 文件描述符的數組
if (connect(sock, (sockaddr*)&saddr, sizeof(saddr)) != 0) {
FD_ZERO(&set);// 每次判斷前必須要清空
FD_SET(sock, &set);
timeval tm;
tm.tv_sec = 0;
tm.tv_usec = timeoutms * 1000;
if (select(sock + 1, 0, &set, 0, &tm) <= 0) {
// 只要有一個可寫,就會返回文件描述符的值,否則返回-1,超時返回0
printf("connect timeout or error!\n");
printf("connect %s:%d failed!: %s\n", ip, port, strerror(errno));
return false;
}
}
SetBlock(true);
printf("connect %s:%d succeded!\n", ip, port);
return true;
}
bool XTcp::SetBlock(bool isblock)
{
if (sock <= 0) {
return false;
}
#ifdef WIN32
unsigned long ul = 0;
if (!isblock) {
ul = 1;
}
ioctlsocket(sock, FIONBIO, &ul);
// 下面是Linux中的設置阻塞方式的代碼
#else
int flags = fcntl(sock, F_GETFL, 0);
if (flags < 0) {
return false;
}
if (isblock) {
flags = flags&~O_NONBLOCK;
}
else {
flags = flags | O_NONBLOCK; // 非阻塞模式
}
if (fcntl(sock, F_SETFL, flags) != 0) {
return false; // 如果不等於0,那么設定失敗
}
#endif
return true;
}
SetBlock是用來設置是否阻塞的,這里因為Windows和Linux系統的設置方式不一樣,所以弄了判定條件,不同系統分別做不同處理。為什么非得要設置非阻塞?因為默認情況下connect是阻塞的,在connect發起的三次握手(是的,調用accept的時候三次握手已經完成了)結束之后才會返回值,因為握手不是瞬間就完成的,所以會需要設定延時功能,但是問題就在這里了,Windows下的延時和Linux下的延時好像是實現的效果是不一樣的,哪怕設置相同。所以才會需要用非阻塞的方式自己另外實現延時的功能。
在非阻塞工作模式下,調用connect會立即返回EINPROCESS錯誤(或者0,即成功建立連接,但是通常不可能,除非連接的是本機),但是三次握手其實還在進行,所以需要使用select來檢查連接是否建立成功。select的規則是這樣的,描述字數組中有一個描述字是可寫的時候就會返回那個描述字的值,否則返回-1或0。所以我們可以在配置好select后判斷select返回的值來判斷是否成功建立連接。之所以能用select這么做就是因為連接成功建立的時候,描述字變為可寫(記住,Linux中所有的東西都被當成文件處理,socket也是),select會在數組中某個描述字變為可寫的時候返回該描述字的值。
然后再提一下select中最后面的&tm位置的參數,這個地方用來設置延時時間,在延時時間內select是阻塞的(即一定要等這個函數執行完才能夠繼續向下執行),所以最終可以實現延時的功能。最后執行完后一定要設置回阻塞狀態,否則會出錯。
總之,如果暫時還理解不了的話可以先跳過select部分,這里只是用來實現延時功能的。
創建連接
在接收到連接請求后,服務端接受連接請求,就會創建一個新的socket來專門進行傳輸數據(其實可以聯想下平時使用瀏覽器訪問網站的時候,雖然都是訪問HTTPS的端口443,但是如果只通過這一個端口來給多個用戶服務的話顯然是不夠用的,所以肯定是另外分配臨時的端口用來傳輸數據,443只是用來接收請求的)。
XTcp XTcp::Accept()
{
XTcp tcp;
sockaddr_in caddr;
socklen_t len = sizeof(caddr);
int client = accept(sock, (sockaddr*)&caddr, &len); // 讀取用戶連接信息,會創建新的socket,用來單獨和這個客戶端通信,后面兩個
// 參數要傳指針,用來返回端口號和地址
if (client <= 0) {
return tcp;
}
printf("accept client %d\n", client);
char *ip = inet_ntoa(caddr.sin_addr);
strcpy(tcp.ip, ip);
tcp.port = ntohs(caddr.sin_port); // short,恰好最大65535
tcp.sock = client;
printf("client ip is %s, port is %d \n", tcp.ip, tcp.port);
return tcp;
}
client其實就是分配的編號,分配好的端口號和地址其實存在caddr中。建立好通信用的連接之后,就可以開始通信了。
接收和發送數據
發送數據
int XTcp::Send(const char* buf, int size) {
int s = 0;
while(s != size) {
int len = send(sock, buf + s, size - s, 0);
if (len <= 0) {
break;
}
s += len;
}
return s;
}
這里要結合計算機網絡的一些基礎只是來看,我在之前的博文有詳細介紹,這里只是簡單說一下。這里其實就是直接將存放在緩存中的數據發送出去,注意的是,TCP是以字節為單位的,所以緩存buf的定義就是char,然后s是索引,這里是每次嘗試一次性發送所有的緩存,所以才是send(sock, buf + s, size - s, 0)(send的定義是int send( SOCKET s,const char* buf,int len,int flags);),len是在收到確認報文之后計算出的接收方已經接收到哪里的長度,即按序連續接收到的數據數量(不懂的話看我的另一篇關於TCP的博文)。在send執行之后會進行判斷,看對方是否接收到了所有的數據,如果沒有就會重新發還沒收到的那部分(由s作為索引決定,buf + s指針指向的后面那部分都是要發送且還沒確認對方已經收到的)。其實這里有點類似滑動窗口,只是前沿沒有推進。
接收數據
recv函數的定義是ret = sock.recv(bBuffer,iBufferLen,0);返回值是已經接收到了的數據量(必須是連續且按序到達的才算)。基本上這個函數就夠用了,所以我們這里只是封裝一下:
int XTcp::Recv(char* buf, int bufsize) {
return recv(sock, buf, bufsize, 0);
}
斷開連接
void XTcp::Close() {
if (sock <= 0) return;
closesocket(sock);
}
就調用一下函數關閉socket,沒什么好說的。
最后補充下析構函數:
XTcp::~XTcp()
{
}
啥都沒,不用搞什么騷操作。
用到的頭文件就是這些:
#include "XTcp.h"
#include <iostream>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#ifdef WIN32
// 兼容Linux
#include <Windows.h>
#define socklen_t int
#else
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <fcntl.h>
#define closesocket close
#endif
#include <thread>
服務端邏輯編寫
#include "XTcp.h"
#include <stdlib.h>
#include <thread>
#include <string.h>
class TcpThread
{
public:
void Main()
{
char buf[1024] = { 0 };
for (;;)
{
int recvlen = client.Recv(buf, sizeof(buf) - 1);
if (recvlen <= 0) break;
buf[recvlen] = '\0';
if (strstr(buf, "quit") != NULL)
{
char re[] = "quit success!\n";
client.Send(re, strlen(re) + 1);
break;
}
int sendlen = client.Send("ok\n", 4);
printf("recv %s\n", buf);
}
client.Close();
delete this;
}
XTcp client;
};
int main(int argc, char *argv[]) {
unsigned short port = 8080;
if (argc > 1) {
port = atoi(argv[1]);
}
XTcp server;
server.CreateSocket();
server.Bind(port);
for (;;)
{
XTcp client = server.Accept();
TcpThread *th = new TcpThread();
th->client = client;
//創建線程
std::thread sth(&TcpThread::Main, th);
//釋放父線程擁有的子線程資源
sth.detach();
}
server.Close();
getchar();
return 0;
}
這里用創建新線程的方式為多個用戶提供服務,大概了解下就行,不創建新進程也可以,只是會只能等一個用戶斷開連接之后新用戶才能連接。服務端我是放在Linux服務器上的,但是makefile就不放出來了,這個比較簡單。
客戶端邏輯編寫
#include "XTcp.h"
#include <stdlib.h>
#include <iostream>
int main() {
XTcp client;
client.CreateSocket();
//client.SetBlock(true);
client.Connect("192.168.56.102", 8080);// ip地址和端口可以改成自己想要的,記得設置防火牆放行對應的端口
client.Send("client", 6);
char buf[1024] = { 0 };
client.Recv(buf, sizeof(buf));
printf("%s\n", buf);
getchar(); // 只是用來暫停程序看效果的
return 0;
}
最終效果

這里只是互相傳了一段文字,怎么改的話就不多說了,嗯。
參考
socket函數send和recv函數
C++socket網絡編程大全:講解挺透徹,建議購買學習。大部分內容來自這個課程,其實課程中還有關於動態鏈接庫的生成部分,值得一看,但是這里就不放出相關內容了,想看的話還是掏錢買吧(不到200的價格,要啥自行車)
socket編程之select:介紹了select函數,值得一看
socket通信中select函數的使用和解釋:也是關於select的,感興趣的可以去看看。還設計了點組阻塞的內容
非阻塞socket編程:值得一看,這里涉及的內容更廣一些
