C++寫Socket——TCP篇(0)建立連接及雙方傳輸數據


滿山的紅葉……飄落之時……

最近接觸了點關於用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是有定義的,所以會自動執行ifdefendif之間的代碼,初始化庫。如果是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編程:值得一看,這里涉及的內容更廣一些


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM