zoukankan      html  css  js  c++  java
  • 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!
    ");
    	}
    	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!
    ");
    			printf("connect %s:%d failed!: %s
    ", ip, port, strerror(errno));
    			return false;
    		}
    	}
    	SetBlock(true);
    	printf("connect %s:%d succeded!
    ", 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
    ", 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 
    ", 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] = '';
    
    			if (strstr(buf, "quit") != NULL)
    			{
    				char re[] = "quit success!
    ";
    				client.Send(re, strlen(re) + 1);
    				break;
    			}
    			int sendlen = client.Send("ok
    ", 4);
    			printf("recv %s
    ", 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
    ", buf);
    	getchar(); // 只是用来暂停程序看效果的
    	return 0;
    }
    

    最终效果

    这里只是互相传了一段文字,怎么改的话就不多说了,嗯。

    参考

    socket函数send和recv函数
    C++socket网络编程大全:讲解挺透彻,建议购买学习。大部分内容来自这个课程,其实课程中还有关于动态链接库的生成部分,值得一看,但是这里就不放出相关内容了,想看的话还是掏钱买吧(不到200的价格,要啥自行车)
    socket编程之select:介绍了select函数,值得一看
    socket通信中select函数的使用和解释:也是关于select的,感兴趣的可以去看看。还设计了点组阻塞的内容
    非阻塞socket编程:值得一看,这里涉及的内容更广一些

  • 相关阅读:
    超级简单:一步一步教你创建一小型的asp.net mvc 应用程序
    asp.net AJAX 验证用户名是否存在 Jquery
    生成缩略图、为图片添加文字水印、图片水印的类
    图Graph
    [转]Implementing a Generic Binary Tree in C#
    .net C#数据结构
    Why HTML5 is worth your time
    跳跃表SkipList
    C# LockFreeStack类
    [转]泛型弱引用
  • 原文地址:https://www.cnblogs.com/yejianying/p/cpp_socket_0.html
Copyright © 2011-2022 走看看