您的位置:首页 > 编程语言 > C语言/C++

一步一步从原理跟我学邮件收取及发送 5.C语言的socket示例

2018-01-26 12:45 162 查看
一步一步从原理跟我学邮件收取及发送 5.C语言的socket示例

    说到 C 语言版本的程序,首先要解决的问题就是兼容性. 作为 20 年开发有 10 多年是在服务端的程序员,我深刻地感受到服务端平台的两极分化之严重,linux 派对 windows 那是超级的不屑一顾:那都是没技术的人才用的,没能力维护 linux 的人才用 windows. 与此同时 windows 派对 linux 也是嗤之以鼻,我曾经的一位经理就时常不屑对我说,我就不信那几个人写的东西能比公司写的好. 奇怪的这两派其实都很能挣钱,BAT 什么的都用 linux 我们就不说了,但股票期货交易这样重要的而且性能要求一样很高的行业内几乎一水的
windows + sql server 恐怕大家就不知道了吧. 所以我真不太同意 linux 性能就比 windows 高的说法.

    我觉得形成这种说法的很重要的一个因素是很多高性能的软件没有 windows 版本,比如 nginx 长期不推荐在 windows 下使用, redis 下的 windows 版本居然是微软自己拿过来修改过才能用的.到底真相如何我们就不讨论了,单就为什么这些软件没有 windows 版本,我觉得一个很重要的原因是 C/C++ 语言在两种平台下的兼容性问题.开源界现在大量的用 gcc,而 gcc 的语法现在和 vc 的语法差别是越来越大,我过去经常在 pc 中引用开源代码,有些代码花上一整天的没法在 vc
中编译通过(印象中最好编译的是 apache 的代码).我个人觉得既然开源了,还是应该考虑一下 vc 的兼容性(当然了个人时间是有限的,我写的很多东西也都没有考虑,基本上手上的平台下能编译过去也就算了...).

    这种兼容性体现在很多方面,第一步选择 ide (或者称不上 ide 的开发工具) 时基本上都会要求引入库文件. linux 下是 so 或者 a 文件,这里就不说了,单只讨论 windows 下的就有很多区别. 传统 vc 下是要引入 lib 文件,而现在有大量基于 gcc 的多种开发工具,它们要引入的是 a 文件,它们是不通用的(小提示:有些版本的 gcc 能使用 lib 文件).所以如果是拿一个 vc 的示例,那么在 gcc 系的开发工具中是用不了的. 我的解决办法是不用 socket 的库文件!
初学者还没什么,有经验的同学们又要炸锅了:可能吗! 没什么的可能的,前面已经说了这些 socket 函数是操作系统提供的,与开发语言无关,我们其实可以直接使用操作系统的功能,这种"直接使用"也没什么稀奇的就是直接调用 dll  文件罢了,delphi 的所有 socket 都是这样使用的.具体的调用方法就是直接调用 dll 中的函数指针,这在所有的 windows api 开发书籍中都会讲到,一点也不稀奇.本质上各个编译器最后也是要这样调用的,只不过它们按照传统把这种操作弄到了库文件中了而已.

我先上代码,大家先别急着看,我后面会讲解,其实也都挺简单的.

(文件名 socketplus.c)

复制代码

//一个方便测试 socket 程序的小文件,省得老是找 lib a 文件 

//目前是 gcc 专用,如果 vc 要用另外弄一个好了,不要在这上面弄条件编译 

#ifndef _SOCKET_PLUS_C_

#define    _SOCKET_PLUS_C_

#include <stdio.h>

#include <windows.h>

#include <time.h>

#include <winsock.h>

//#include <>

#include "lstring.c"

//#pragma comment (lib,"*.lib")

//#pragma comment (lib,"libwsock32.a")

//#pragma comment (lib,"libwsock32.a")

// 系统错误信息提示

void PrintError(DWORD last_err);

//--------------------------------------------------

//直接引入的 dll 函数 

//SOCKET PASCAL socket(int,int,int);

//char * (*fun1)(char * p1,char * p2);

#ifndef _MSC_VER

//很多同学不会写函数指针声明//函数指针的写法是,先写正常的函数声明,然后将函数名加上括号,然后在函数名前再加上*号即可!!! 

SOCKET PASCAL (*_socket)(int,int,int);

//SOCKET (PASCAL *_socket)(int,int,int); //vc 要这样写,vc6,vc2010 都是如此 //就是将 PASCAL 或者 stdcall 放到函数名的括号中 

int PASCAL (*_WSAStartup)(WORD,LPWSADATA);

unsigned long PASCAL (*_inet_addr)(const char*);

u_short PASCAL (*_htons)(u_short);

int PASCAL (*_connect)(SOCKET,const struct sockaddr*,int);

int PASCAL (*_WSAGetLastError)(void);

int PASCAL (*_send)(SOCKET,const char*,int,int);

int PASCAL (*_recv)(SOCKET,char*,int,int);

int PASCAL (*_select)(int nfds,fd_set*,fd_set*,fd_set*,const struct timeval*);

struct hostent * PASCAL (*_gethostbyname)(const char*);

#endif

#ifdef _MSC_VER

SOCKET (PASCAL *_socket)(int,int,int); //vc 要这样写,vc6,vc2010 都是如此//就是将 PASCAL 或者 stdcall 放到函数名的括号中 

int  (PASCAL *_WSAStartup)(WORD,LPWSADATA);

unsigned long  (PASCAL*_inet_addr)(const char*);

u_short  (PASCAL*_htons)(u_short);

int  (PASCAL *_connect)(SOCKET,const struct sockaddr*,int);

int  (PASCAL *_WSAGetLastError)(void);

int  (PASCAL *_send)(SOCKET,const char*,int,int);

int  (PASCAL *_recv)(SOCKET,char*,int,int);

int  (PASCAL *_select)(int nfds,fd_set*,fd_set*,fd_set*,const struct timeval*);

struct hostent *  (PASCAL *_gethostbyname)(const char*);

#endif

//-------------------------------------------------- 

int CreateTcpClient()

{

    //LoadLibrary("wsock32.dll");

    return _socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

}

int InitWinSocket()

{

  WSADATA wData;

  //Result := WSAStartup(MakeWord(2, 2), wData) = 0;

  return _WSAStartup(MAKEWORD(2, 2), &wData);

  

}

int ConnectIP(SOCKET so, char * ip, int port)

{

  //sock: TSocket;

  //SockAddr: TSockAddr;

  //struct sockaddr SockAddr;

  struct sockaddr_in SockAddr;

  int err;

  int Result = 1;

  memset(&SockAddr, 0, sizeof(SockAddr));

  SockAddr.sin_family = AF_INET;

  SockAd
e54e
dr.sin_port = _htons(port);

  SockAddr.sin_addr.s_addr = _inet_addr(ip);

  //if (_connect(so, (struct sockaddr *)(&SockAddr), sizeof(SOCKADDR_IN)) == SOCKET_ERROR) 

  if (_connect(so, &SockAddr, sizeof(SOCKADDR_IN)) == SOCKET_ERROR) //其实是不用转换的 

  {

      PrintError(0);

    err = _WSAGetLastError(); //其实这个和 GetLastError 是一样的

    //ShowMessageFmt('connect socket error,[%d]', [WSAGetLastError]);

    MessageBox(0, "connect socket error", "", 0);

    Result = 0;

    

    //int err = _WSAGetLastError();//如果前面调用了别的 api 这里是取到 0 的,而不是错误信息码 

    

    PrintError(err);

    

    if (INVALID_SOCKET == so) printf("connect error:INVALID_SOCKET\r\n");

    

    return 0;

  }

  

  return 1;

}//

//clq 这是我新加的函数,目的是可以根据域名来访问,并且原来的代码只能访问局域网

int ConnectHost(SOCKET so, char * host, int port)

{

    const char * address = host;

    int is_connect = 0;

    int err = 0; 

    

    // Create an address structure and clear it

    struct sockaddr_in addr;

    memset(&addr, 0, sizeof(addr));

    

    // Fill in the address if possible//先尝试当做IP来解析

    addr.sin_family = AF_INET;

    addr.sin_addr.s_addr = _inet_addr(address);

    

    // Was the string a valid IP address?//如果不是IP就当做域名来解析

    if (addr.sin_addr.s_addr == -1)

    {

        // No, so get the actual IP address of the host name specified

        struct hostent *pHost;

        pHost = _gethostbyname(address);

    

        if (pHost != NULL)

        {

            if (pHost->h_addr == NULL)

                return 0;//false;

                

            addr.sin_addr.s_addr = ((struct in_addr *)pHost->h_addr)->s_addr;

        }

        else

            return 0;//false;

    }    

    addr.sin_port = _htons(port);

    

    //返回:-1 连接失败;0 连接成功

    if (_connect(so, (struct sockaddr *)&addr, sizeof(addr)) == 0)

    {

        is_connect = 1;//true;

    }

    else

    {

        is_connect = 0;//false;

        //连接失败

        //printfd3("WSAGetLastError:%d, WSAEWOULDBLOCK:%d\r\n", WSAGetLastError()-WSABASEERR, WSAEWOULDBLOCK-WSABASEERR);

        

          PrintError(0);

        err = _WSAGetLastError(); //其实这个和 GetLastError 是一样的

        //ShowMessageFmt('connect socket error,[%d]', [WSAGetLastError]);

        MessageBox(0, "connect socket error", "", 0);

        is_connect = 0;

        

        //int err = _WSAGetLastError();//如果前面调用了别的 api 这里是取到 0 的,而不是错误信息码 

        

        PrintError(err);

        

        if (INVALID_SOCKET == so) printf("connect error:INVALID_SOCKET\r\n");        

    }

    

    return is_connect;

}//

#ifndef faveLoadFunction

#define faveLoadFunction

FARPROC WINAPI LoadFunction(HINSTANCE h, LPCSTR fun_name)

{

    FARPROC WINAPI r = 0;

    

    r = GetProcAddress(h, fun_name);

    

    if (r == 0) printf("load function %s error\r\n", fun_name);

    else printf("load function %s ok\r\n", fun_name);    

    return r;

}//

#endif

void LoadFunctions_Socket()

{

    //HINSTANCE hs = LoadLibrary("wsock32.dll"); //根据不同的编译环境,有可能要从 LoadLibrary 改成 LoadLibraryA

    HINSTANCE hs = LoadLibraryA("wsock32.dll"); //根据不同的编译环境,有可能要从 LoadLibrary 改成 LoadLibraryA

    

    if (hs == 0) printf("load wsock32.dll error\r\n", hs);

    else printf("load wsock32.dll ok\r\n", hs);

    

    _socket = GetProcAddress(hs, "socket");

    

    printf("_socket:%d\r\n", _socket);

    if (_socket == 0) printf("load _socket error\r\n", hs);

    

    //-------------------------------------------------- 

    //直接装载各个 dll 函数 

    _socket = LoadFunction(hs, "socket");

    _WSAStartup = LoadFunction(hs, "WSAStartup");

    _inet_addr = LoadFunction(hs, "inet_addr");

    _htons = LoadFunction(hs, "htons");

    _connect = LoadFunction(hs, "connect");

    _WSAGetLastError = LoadFunction(hs, "WSAGetLastError");

    _send = LoadFunction(hs, "send");

    _recv = LoadFunction(hs, "recv");

    _select = LoadFunction(hs, "select");

    _gethostbyname = LoadFunction(hs, "gethostbyname");

    

    

}

// 系统错误信息提示

void PrintError(DWORD last_err)

{

    //进行出错。

    //if (!CreateDirectory(_T("c:\\"),0))

    {

        char buf[512];//char buf[128];

        LPVOID lpMsgBuf;

        DWORD dw;

        

        memset(&buf, 0, sizeof(buf));

        

        dw = last_err;//GetLastError();

        

        if (dw == 0) dw = GetLastError(); //实际上是可以代替 WSAGetLastError 的 

        

        //dw = 5;//10035;//6000054;

        

        if (dw == 0) return;

        

        FormatMessage (

            FORMAT_MESSAGE_FROM_SYSTEM,//FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, //FORMAT_MESSAGE_ALLOCATE_BUFFER 是指要分配内存 

            NULL,

            dw,

            MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),

            (LPTSTR) &buf,//(LPTSTR) &lpMsgBuf,

            500,//0, //因为是自己分配的内存,所以要指出分配了多大 

            NULL );

            

        printf("PrintError(出错码=%d):%s\r\n", dw, buf); //奇怪,这里就是不对//是倒数第 2 个 参数的问题 

            

        FormatMessage (

            FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, //FORMAT_MESSAGE_ALLOCATE_BUFFER 是指要分配内存 

            NULL,

            dw,

            MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),

            (LPTSTR) &lpMsgBuf,

            0, NULL );

            

         wsprintf(buf,

                    "出错信息 (出错码=%d): %s",

                    dw, lpMsgBuf);             

            

        printf("PrintError(出错码=%d):%s\r\n", dw, lpMsgBuf);

        //printf("PrintError(出错码=%d):%s\r\n", dw, &buf); //奇怪,这个是不对的 

        printf("PrintError(出错码=%d):%s\r\n", dw, buf);

            

        LocalFree(lpMsgBuf);

        

        //输出提示

        //OutputDebugString(szBuf);

    }

}//

//尚未精测试,可能有误

int SendBuf(SOCKET so, char * s, int len)

{

  int r = 0;

  int count = 0;

  char * p = s;

  //r := 0;

  int Result = 0;

  //count := 0;

  //p := @s[1];

  while (Result<len)

  {

    r = _send(so, p, len - Result, 0);

    if  (r > 0)

    {

      Result = Result + r;

      p = p + r;

      

    };

    count++;

    if (count>10) //超过多少次就不发送了 

    {

      MessageBox(0, "send error", "", 0);

      return Result;

    }

  }

  

  return Result;

}//

//注意,返回的字符串要自己释放 

//lstring RecvBuf(SOCKET so)

//算了,还是传可自动释放的字符串进去方便点 

//void RecvBuf(SOCKET so, lstring * buf)//用这个格式也可以,不过与其他语言不通用 

lstring * RecvBuf(SOCKET so, struct MemPool * pool)

{

  char buf[1024+1];

  int r = 0;

  //lstring s = String("");

  //lstring * s = NewString("", _buf->pool);

  //CheckPString(_buf);

  lstring * s = NewString("", pool);

  memset(&buf, 0, sizeof(buf));

  r = _recv(so, buf, sizeof(buf)-1, 0); //留下一个 #0 结尾

  if (r > 0) 

  {

    //SetLength(s, r);

    //Move(buf, s[1], r);

    //s.Append(&s, StringConst(buf, r));

    LString_AppendCString(s, buf, r);

  }

  if (r == 0) //一般都是断开了

  {

    MessageBox(0, "recv error.[socket close]", "", 0);

    //return String(""www.qwj127.com); //不应该再生成一个新的

    return s;

  }

  return s;

}//

//是否可读取

int SelectRead(SOCKET so)

{

  fd_set fd_read; //fd_read:TFDSet;

  struct timeval timeout; // : TTimeVal;

  int Result = 0;

  FD_ZERO( &fd_read );

  FD_SET(so, &fd_read ); //个数受限于 FD_SETSIZE 

  timeout.tv_sec = 0; //秒

  timeout.tv_usec = 500;  //毫秒

  timeout.tv_usec = 0;  //毫秒

  

      //linux 第一个参数一定要赋值

    ////int r = ::select(socket_handle+1, &fd_read, NULL, NULL, &l_timeout);

  //if select( 0, &fd_read, nil, nil, &timeout ) > 0 then //至少有1个等待Accept的connection

  if (_select( so+1, &fd_read, NULL, NULL, &timeout ) > 0) //至少有1个等待Accept的connection

    Result = 1;

    

  return Result;    

}//

#endif

复制代码

 

当然,我这些代码倒也不是为了写示例方便大家测试,而是因为工作的关系我需要在多个 ide 和多种编译器中切换,早就写好用来测试网络应用的(主要是xmpp协议,所以以后我们也顺便介绍一下 xmpp 协议的一些简单实现).

其中有以下几点需要说明一下:

1.函数需要从 dll 中取出其地址,在代码中实现为 LoadFunctions_Socket();

2.LoadLibraryA/LoadLibrary 函数可以理解为将一个 dll 中的代码读取到程序的内存中;

3.GetProcAddress 函数可以理解为找到一个函数的地址;

4.不能直接使用原始的 socket 函数名,要使用时,在前面加一个划线;

5.只引入了我用到的少数几个 socket 函数,有需要的网友可以自己引入:方法是先声明一个同原型的函数指针,然后加入到 LoadFunctions_Socket() 中就可以了.

6.代码主要工作在 windows 版本的 gcc, vc6 和 vc2010 下也做过测试. 随着代码的增加 vc 下如果不能运行,大家可以自己改下函数指针的声明和 api 函数的版本(换下 a 和 w 的版本). 至于 a/w 版本是什么意思,不懂的同学我们以后再说明吧.

7.我直接 include 了 c 的文件,方便在各个开发工具中切换,不喜欢这种方式的同学请自己改成 h 文件的方式吧.

我主要的工作环境是 cfree,原因主要是 vc 产生的临时文件实在是太大了,设置不产生临时文件又会产生别的问题,而用 dev c++ 这些传统的 gcc 环境的话,代码提示又是一个问题. 找了很久我最终选用了 cfree 软件,不过要说明的是 cfree 是收费的,但费用不足百元,所以虽然网上有很多注册码可以用我还是推荐大家注册一下.遗憾的是,国内的共享环境之恶劣导致我付费时很是花了几天时候,最终是找作者要到了他的淘宝账号直接转的钱,作者一度以为我是骗子让我去联系第三方的注册网站,我觉得那个第三方的注册网站更象骗子,所以坚持直接转给他.之所以我认为第三方注册机构更象骗子是因为我也写过共享软件
...

cfree 对我来说有以下吸引力的优点:

1.没有太大的临时文件;

2.代码提示还不错;

3.不需要建立工程文件;

4.注册费用低,我是它的正版用户;

5.通过配置可以把编辑器界面弄得象我喜欢的 vscode;

6.我有一个 32 位的 windows 2003 工作环境,那里用不了 vscode,也不用了现在最新的 vc;

7.作为 gcc 系,几乎不需要配置编译环境.

这里要提一下开源的 codelite,也是不错的软件,代码提示也做得很好,但生成的临时文件大了点,而且需要配置编译环境(对初学者很致命),如果没有 cfree 我就用它了.

另外一定要说的是它们的调试都很差,如果有无法理解的代码,请打开您的 VC ... 不过我现在调试用的是我能找到的最简便的网友简化版的非官方版本 BCB2009 (其实人家叫 CodeGear C++ Builder 2009,bcb 的称呼只是历史原因),因为这个版本安装非常方便,要不我就用 bcb6 了.原版 bcb2009 是否方便我就不清楚了. 找不到简化版本的同学用最常见的 vc6 调试就行了,一样好用.

好了,下面给出测试的 demo 代码. c 语言中还有些要注意的字节对齐等问题也来不及说了,我们下篇再说吧.

复制代码

    gSo = CreateTcpClient();

    r = ConnectHost(gSo, "newbt.net", 25);

    if (r == 1) printf("连接成功!\r\n");

    s = NewString("EHLO\r\n", m);

    SendBuf(gSo, s->str, s->len);

    printf(s->str);

    s->Append(s, s);

    printf(s->str);

    s->AppendConst(s, "中文\r\n");

    printf(s->str);

    rs = RecvBuf(gSo, m); //注意这个并不只是收取一行 

    printf("\r\nRecvBuf:\r\n");

    printf(rs->str);

    

    rs = RecvBuf(gSo, m); //注意这个并不只是收取一行 

    printf("\r\nRecvBuf:\r\n");

    printf(rs->str);

复制代码

这是通讯过程,代码并不复杂,大家可以看到简单封装后并不复杂,所以按自己顺手的封装一下很有必要.

以下是完整代码:

(文件名 socket_test1.c)

复制代码

#include <stdio.h>

#include <windows.h>

#include <time.h>

#include <winsock.h>

#include "lstring.c"

#include "socketplus.c"

#include "lstring_functions.c"

//vc 下要有可能要加 lib 

//#pragma comment (lib,"*.lib")

//#pragma comment (lib,"libwsock32.a")

//#pragma comment (lib,"libwsock32.a")

//SOCKET gSo = 0;

SOCKET gSo = -1;

void main()

{

    int r;

    mempool mem, * m;

    lstring * s;

    lstring * rs;

    

    //--------------------------------------------------

    mem = makemem(); m = &mem; //内存池,重要 

    //--------------------------------------------------

    //直接装载各个 dll 函数

    LoadFunctions_Socket();

    InitWinSocket(www.huayu521.com ); //初始化 www.huange157.com  socket, windows 下一定要有 

    gSo = CreateTcpClient(www.huayuyulee.cn);

    r = ConnectHost(gSo, "newbt.net", 25);

    if (r == 1) printf("连接成功!\r\n");

    s = NewString("EHLO\r\n", m);

    SendBuf(gSo, s->str, s->len);

    printf(s->str);

    s->Append(s, s);

    printf(s->str);

    s->AppendConst(s, "中文\r\n");

    printf(s->str);

    rs = RecvBuf(gSo, m); //注意这个并不只是收取一行 

    printf("\r\nRecvBuf:\r\n");

    printf(rs->str);

    

    rs = RecvBuf(gSo, m); //注意这个并不只是收取一行 

    printf("\r\nRecvBuf:\r\n");

    printf(rs->str);

    //--------------------------------------------------

    

    Pool_Free(&mem); //释放内存池 

    

    printf("gMallocCount:%d \r\n", gMallocCount); //看看有没有内存泄漏//简单的检测而已  

    

    //-------------------------------------------------- 

    getch(); //getch(www.jiumenyuLe1.com ).不过在VC中好象要用getch(),必须在头文件中加上<conio.h> 

}

复制代码

除了通讯 demo 部分外,其他代码大家都不用细看,都是临时的辅助封装函数而已,用到生产环境中后果我可不负责(虽然我认为改一改也是可以用的).就当是抛砖引玉吧.

代码里其实还用到了一个很重要的字符串类,不过也是来不及解说了,先给出代码如下:

(文件名为 lstring.c 和 lstring_functions.c,本来我是要放到 github 上的,不过估计也是没时间维护了,所以直接贴一下吧)

复制代码

//没办法还是得另定义一个字符串 

#ifndef _L_STRING_C_

#define    _L_STRING_C_

#include <stdio.h>

#include <malloc.h> //有些编译,如果 malloc.h 在后面的话会报 malloc 函数冲突,解决办法很简单,把含有 malloc 的头文件放前面,好让我们的 malloc 定义能覆盖它就可以了

#include <string.h>

//#include <time.h>

//#include <winsock.h>

#include <windows.h>

#include <time.h>

//#include <crt/eh.h> /www.huayiyuL.com/据说 MinGW终于支持使用seh实现c++ eh了 

//https://sourceforge.net/p/mingw-w64/mingw-w64/ci/18a7e88bcbe8bc0de4e07dac934ebf0653c4da7c/tree/mingw-w64-headers/crt/eh.h

int gMallocCount = 0;

//简单的内存泄漏检测

void * malloc_v2(size_t size)

{

    gMallocCount++;

    return malloc(size);

}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐