满山的红叶……飘落之时……
最近接触了点关于用C 写socket的东西,这里总结下。
这里主要是关于TCP的,TCP的特点什么的相关介绍在我另一篇博文里,所以这里直接动手吧。
我们先在windows下写,不过代码可以直接移植到linux下。
Visual Studio项目配置及初始化
这里用的版本是2015的。创建了项目之后要配置项目的属性:
在下图箭头处添加ws2_32.lib
,不然没办法使用socket相关的函数。
然后在win平台下,使用这个库前需要初始化,因此在main函数中应有:
代码语言:javascript复制WSADATA ws;
WSAStartup(MAKEWORD(2, 2), &ws);
不过因为只是在win平台下才需要编译,所以可以这样写:
代码语言:javascript复制#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函数中初始化,但是为了后面拓展方便,最好封装在类中。类的构造函数如下:
代码语言:javascript复制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
}
这里说一下类的头文件/声明:
代码语言:javascript复制#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
注意,因为是双方都可以收发,所以必须是双方都有一个用来接收的函数,一个发送的函数。其实这里写的服务端代码和客户端代码是一样的,如果读者有兴趣的话再自行拓展。
配置服务端
在配置之前先弄清楚大概是怎么个流程。首先我们会监听一个端口,这个端口只是用来接收请求然后建立连接的,但是不会用来传输数据。客户端请求之后服务器会另外分配一个端口,客户端和服务端是通过这个新分配的端口来进行通信的。
监听指定端口
了解了大概的流程之后我们就可以开始编写了,首先是监听和建立连接的部分:
代码语言:javascript复制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地址、超时时间即可。
代码语言:javascript复制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!: %sn", 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只是用来接收请求的)。
代码语言:javascript复制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 %dn", 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
中。建立好通信用的连接之后,就可以开始通信了。
接收和发送数据
发送数据
代码语言:javascript复制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);
}
断开连接
代码语言:javascript复制void XTcp::Close() {
if (sock <= 0) return;
closesocket(sock);
}
就调用一下函数关闭socket,没什么好说的。
最后补充下析构函数:
代码语言:javascript复制XTcp::~XTcp()
{
}
啥都没,不用搞什么骚操作。
用到的头文件就是这些:
代码语言:javascript复制#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>
服务端逻辑编写
代码语言:javascript复制#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] = '