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
是有定义的,所以会自动执行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编程:值得一看,这里涉及的内容更广一些
C++写Socket——TCP篇(0)建立连接及双方传输数据的更多相关文章
- 从Linux源码看Socket(TCP)的listen及连接队列
从Linux源码看Socket(TCP)的listen及连接队列 前言 笔者一直觉得如果能知道从应用到框架再到操作系统的每一处代码,是一件Exciting的事情. 今天笔者就来从Linux源码的角度看 ...
- socket TCP 从0实现音频传输 ALSA 播放
RTP标准是采用 UDP 发送,有不少现成的开源库,但不在本文讨论的范围内.UDP 用户数据报,不提供流程,安全传输的功能,但速度快,能提供多播,广播,没有序列号 SEQ ,有 MTU 限制,1500 ...
- Socket小白篇-附加TCP/UDP简介
Socket小白篇-附加TCP/UDP简介 Socket 网络通信的要素 TCP和UDP Socket的通信流程图 1.Socket 什么是Socket Socket:又称作是套接字,网络上的两个程序 ...
- 从Linux源码看Socket(TCP)的accept
从Linux源码看Socket(TCP)的accept 前言 笔者一直觉得如果能知道从应用到框架再到操作系统的每一处代码,是一件Exciting的事情. 今天笔者就从Linux源码的角度看下Serve ...
- 分享一个分布式消息总线,基于.NET Socket Tcp的发布-订阅框架,附代码下载
一.分布式消息总线 在很多MIS项目之中都有这样的需求,需要一个及时.高效的的通知机制,即比如当使用者A完成了任务X,就需要立即告知使用者B任务X已经完成,在通常的情况下,开发人中都是在使用者B所使用 ...
- 基于.NET Socket Tcp的发布-订阅框架
基于.NET Socket Tcp的发布-订阅框架 一.分布式消息总线 在很多MIS项目之中都有这样的需求,需要一个及时.高效的的通知机制,即比如当使用者A完成了任务X,就需要立即告知使用者B任务X已 ...
- Oracle建立连接的过程分析
Oracle建立连接的过程 如果我们想登陆数据库并在数据库中真正做事情,就必须先建立连接,首先我会介绍如何建立连接,再介绍建立连接的两种方式的原理,以及建立连接的过程中在客户端和服务端都做了些什么. ...
- 网络编程懒人入门(八):手把手教你写基于TCP的Socket长连接
本文原作者:“水晶虾饺”,原文由“玉刚说”写作平台提供写作赞助,原文版权归“玉刚说”微信公众号所有,即时通讯网收录时有改动. 1.引言 好多小白初次接触即时通讯(比如:IM或者消息推送应用)时,总是不 ...
- 【Python】如何基于Python写一个TCP反向连接后门
首发安全客 如何基于Python写一个TCP反向连接后门 https://www.anquanke.com/post/id/92401 0x0 介绍 在Linux系统做未授权测试,我们须准备一个安全的 ...
随机推荐
- Linux下Mongodb的安装
[root@localhost usr]# cd /usr //创建Mongodb目录 用于存放Mongodb的程序 [root@localhost usr]# mkdir mongodb [root ...
- nodejs之express入门
首先安装nodejs,官网下载安装包直接安装, 由于node自带npm,所以npm命令直接即可使用 打开cmd,使用npm install -g express-generator安装express ...
- C# SqlDataReader
var reader = MSSQLHelper.ExecuteReader(sb.ToString(), out erro, ConnectionString); //SqlDataReader / ...
- vs 2019 调试web项目 浏览器
- HDU 6669 Game
hdu题面 解题思路 首先我们要选一个起点,这个起点应该在第一个区间内,然后再看第二个区间在左边还是右边以便移动,但转念一想,我们可以把起点直接选在前一堆区间的交集上,于是思路就有了--依次把所有区间 ...
- mysql 链接数满了的错误 ERROR 1040 (HY000): Too many connections
mysql 链接数满了的错误 ERROR 1040 (HY000): Too many connections 第一种处理方式: ./mysql -u root -p 登录成功后执行以下语句查询当前的 ...
- js+jq 淡入淡出轮播(点击+定时+鼠标进入移出事件)
<!DOCTYPE html><html> <head> <meta charset="utf-8" /> <title> ...
- ping不通服务器就邮件警告
ping不通服务器就发一封邮件 (单台) vim /root/scipt/1.sh #!/bin/bash ping -c 4 192.168.0.116 &> /dev/null #p ...
- nginx负载均衡 之集群概念与负载均衡
集群介绍 为什么要用集群
- Zookeeper(二)数据模型
Zookeeper数据模型ZNode 问题 ZK的数据模型ZNodes是什么样的: 树形结构,每个叶子节点都存储着数据,且可挂载子节点: 路径作为叶子节点名称,数据作为叶子节点内的数据: Znode可 ...