Socket网络编程基础

首先来写点网络基础知识。

什么是地址?

在网络通信中,两个进程分别位于不同的机子上,在互联网中,两台机子不能位于不同的网络,这些网络通过网络连接设备连接(网关,网桥,路由器)。

所以,要找到正确的地址,需要进行三级寻址:

  1. 某一主机可与多个网络连接,必须指定该主机唯一的标识符。
  2. 网络上,每台主机应该有其唯一的地址。
  3. 每一台主机上的每一个进程应该有在该主机上的唯一标识符。

什么是IP地址和端口?

IP地址:用于在局域网或者互联网中区分不同主机的编号。它用点分十进制法来表示,分4段,每段8位,总长为32位长,如:192.163.0.12。IP地址指定的不是主机,而是网络接口设备。若一个主机有两个网络接口,那么就会有2个IP地址。一般情况,我们的主机都只有一个网络接口设备,所以只有一个IP地址,IP地址可以在dos窗口中用ipconfig命令查看。对于服务器来说,则会有多个网络接口设备,每个网络接口设备都会有一个IP地址。

端口(PORT):在同一台主机里,可能会有各种不同的网络通讯软件,一个端口在同一时间内只能给某一个软件的进程使用。各种不同的软件占用了各自不同的端口之后,数据送达到这台主机后,就会自动按端口进行分理输送。在OSI七层模型中,传输层与网络层的主要区别就是传输层提供进程通信的能力。所以,网络通信最终的地址只靠主机地址是不够的,还要包括一个描述进程的标识符。基于此,TCP/IP协议提出了协议端口的概念,用这个来标识通信的进程。

TCP/UDP协议:
在传输层上,有两大协议,分别是TCP协议和UDP协议。

TCP协议是一种面向连接的,可靠的通信协议,它遵循三次握手原则,保证资源被准确送到目的地址。

UDP协议是一种无连接的传输层协议,提供面向事务的简单不可靠信息的传送。它效率高,但不能保证传送准确性,不可靠。

TCP端口和UDP端口各有65536个,一般小于1024的为保留端口,我们一般使用大于1024的端口。

常见的端口有:
TCP:
FTP 21
Telnet 23
SMTP 25
HTTP 80
POP3 110

UDP:
DNS 53
Tftp 69

大端和小端模式:
不同的计算机存放多字节值的顺序不同,分为大端模式和小端模式。

  • 大端模式为:内存高地址存放数据低位字节数据,内存低地址存放数据高位字节数据。

  • 小端模式为:内存高地址存放数据高位字节数据,内存低地址存放数据低位数据。

如:一个数为0x10203040,则:

大端模式存储为:10 20 30 40

小端模式存储为:40 30 20 10

要判断主机字节序,我这说两种方法:

方法一:

DWORD dwSmallNum = 0x01020304;
if(*(BYTE)&dwSmallNum == 0x04)
{
    std::cout << "小端模式" << std::endl; 
} 
else
{
    std::cout << "大端模式" << std::endl; 
} 

方法二:

DWORD dwSmallNum =  0x01020304;
if(dwSmallNum == htonl(dwSmallNum))
{
    std::cout << "大端模式" << std::endl; 
} 
else
{
    std::cout << "小端模式" << std::endl; 
} 

面向连接的协议:
面向连接的协议很像打电话,两台计算机在第一次进行数据收发之前,需要先在两者之间建立一个通信通道,这样便可确保两台计算机在一条路径上可以互相沟通,其本质上,连接为一个管道,它收发数据不但顺序一致,而且内容完全相同。我们在打电话时,需要拨打号码,别人接听后,便可沟通,沟通完毕,便可挂断电话。

面向连接使用的是TCP协议,服务端与客户端需要的Winsock函数如下:

服务端:

socket() --> bind() --> listen() --> accept() -->send()/recv() --> closesocket()

客户端:

socket --> connect() --> send()/recv() -->closesocket()

非面向连接的协议:

这种方式下,要发送数据,只需直接将要发送的数据传出即可,无需管是否送到。客户端在收到后,也不会有任何回应。怎么样,是不是感觉有点像寄信呢?

非面向连接的协议使用的是UDP协议,服务端与客户端的winsock函数如下:

服务端:

socket() --> bind() --> sendto() / recvfrom() --> closesocket()

客户端:

socket() --> sendto() / recvfrom() --> closesocket()

什么是套接字?

计算机本身是不能互相通讯的,而是计算机软件之间,在同一主机或不同主机之间进行通讯,数据之间的传输是在软件之间进行的。套接字,通俗地讲,它类似于文件指针,是一种用于软件之间数据收发的句柄。

一、Winsocket库的初始化:

int WSAStartup (
    WORD wVersionRequested,
    LPWSADATA lpWSAData
);

要使用Winsock的相关函数,我们先要对Winsock进行初始化,在使用完后,还要进行释放。

  • wVersionRequested:这个 参数表示winsock库的版本号,一般为2.2。
  • lpWSAData:这是一个指向WSADATA的指针,返回0为成功。

释放函数:

int  WSACleanup (void);

二、套接字的创建与关闭:

SOCKET socket (
    int af,
    int type,
    int protocol
);

创建一个套接字,也就是创建一个网络通讯句柄的意思。socket只是返回你以后在系统调用中可能用到的socket描述符,错误时返回-1。使用WSAGetLastError()可以查看错误原因。

af:这个参数用来表示地址族,在window下可用的有AF_INET和PF_INET,这两个宏在winsock2.h下的定义是相同的。

image

AF表示地址族,PF表示协议族。对于windows来说,它们没有区别。而在unix/linux中,它们是不相同的。一般我们在调用socket函数时应该使用PF_INET,在设置地址时使用AF_INET。

  • type:这个参数用于告诉内核使用哪种套接字描述符的类型。通常有3个值:
  • SOCK_STREAM 流套接字
  • SOCK_DGRAM 数据包套接字
  • SOCK_RAW 原始协议接口

protocol:这个参数用来指定应用程序所使用的通信协议。也有3个值:

  • IPPROTO_TCP
  • IPPROTO_UDP
  • IPPROTO_ICMP

这三个值和第二个参数上的三个值是相对应的。第二个参数使用SOCK_STREAM,则第三个参数为IPPROTO_TCP。第二个参数使用SOCK_DGRAM,则第三个参数为IPPROTO_UDP。(注:前面两种情况下,第三个参数可以默认为0)

第二个参数使用SOCK_ICMP,则第三个参数 为IPPROTO_ICMP。

三、地址和信息的绑定

int bind ( 
    SOCKET s, 
    const struct sockaddr FAR* name, 
    int namelen
);

这个函数用来完成套接字与地址端口信息的绑定。就像我们购买了一部手机,要打电话,我们首先应该插上我们的手机卡。

s:第一个参数是新创建的套接字描述符,就是刚才用socket创建的那个。
name:这是一个sockaddr结构体,提供套接字一个地址和端口信息。
namelen:name的大小

struct sockaddr {
    u_short  sa_family; 
    char sa_data[14]; 
};

这个结构共有16个字节,在它之前还要使用一个结构体,定义如下:

struct sockaddr_in{ 
    short sin_family; 
    unsigned short  sin_port; 
    struct in_addr sin_addr; 
    char sin_zero[8];
};

这个结构是为了保持各个特定协议之间的兼容性。向bind()指定地址和端口时,向sockaddr_in中填充相应的内容;调用函数时使用sockaddr。

在sockaddr_in中,还有一个结构体in_addr:

struct in_addr {
    union {
            struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b;
            struct { u_short s_w1,s_w2; } S_un_w;
            u_long S_addr;
    } S_un;

它是一个union S_un,包括一个u_long和两个结构体。IP地址一般是用点分十进制表示的,而in_addr结构体却未提供用来保存IP地址的数据类型,所以需要一个转换函数,把点分表示的ip地址转换为in_addr可以接受的类型。

unsigned long inet_addr (
    const char FAR * cp  
);

使用这个函数可以把点分十进制的ip地址转换为一个unsigned long类型。

char FAR * inet_ntoa (
  struct in_addr in  
);

使用这个函数把unsigned long类型的数据转换为点分十进制表示。

四、监听

int listen (
  SOCKET s,    // 指定要监听的套接字
  int backlog   // 允许进入请求的最大连接数
);

backlog表示允许进入请求的最大连接数, 在winsock中一般由此宏表示:

image

五、接受连接请求

SOCKET accept (
  SOCKET s,                   // 要监听的套接字描述符
  struct sockaddr FAR* addr,  // 指向sockaddr的指针,返回客户端的地址信息
  int FAR* addrlen            // 指向int,传入sockaddr的大小
); 

这个函数用于从连接请求的队列中获取连接信息。

六、连接

int connect (
    SOCKET s,  // 套接字描述符                        
    const struct sockaddr FAR*  name,  // 一个指向sockaddr的指针,其中保存了服务端的IP地址和端口号 
    int namelen  // 指定sockaddr的长度                   
); 

接受请求了,就可以进行连接了。

七、通信

一切就绪,现在便可以进行通信了。

int send (
    SOCKET s,              // accept()返回的套接字描述符          
    const char FAR * buf,  // 发送消息的缓冲区 
    int len,               // buf的长度          
    int flags              // 通常为0          
); 

这个用于发送消息。

int recv (
    SOCKET s,       
    char FAR* buf,  
    int len,        
    int flags       
);

这个用于接收消息,其参数和send()使用方法是一样的。

下面这是UDP的,在UDP中,不需要listen()和accept(),客户端也不需要connect(),通信函数变为这样:

int sendto (
    SOCKET s,  // 套接字描述符                        
    const char FAR * buf,  // 发送数据的缓冲区         
    int len,  // buf的长度                    
    int flags,  // 0                   
    const struct sockaddr FAR * to,  // 这是一个指向sockaddr结构体的指针,给出了接收消息的地址信息
    int tolen  // to的长度                  
);
int recvfrom (  
    SOCKET s,                     
    char FAR* buf,              
    int len,                      
    int flags,                  
    struct sockaddr FAR* from,    
    int FAR* fromlen            
);

用法和sendto相同。

现在就来写一个简单的TCP通信:

服务端:

// SocketServer.h
#include <iostream>
#include <string>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32")

class SocketServer
{
public:                                  
    SocketServer();                      // 初始化,创建,分配
    ~SocketServer();                     // 释放
    void Listen();                       // 监听
    void Accept();                       // 接受
    void SendMsg(const char* message);   //发送
    void RecvMsg();                      //接收

private:
    void PrintError(const char* err) const;

private:
    WSADATA m_wWsaData;
    SOCKET m_hServSock, m_hClntSock;
    SOCKADDR_IN m_sServAddr, m_sClntAddr;
    int m_iClntAddr;
    char m_sMessage[MAXBYTE];
};

// SocketServer.cpp
#include "SocketServer.h"
#pragma warning(disable:4996)

SocketServer::SocketServer()
{
    //初始化套接字
    if (WSAStartup(MAKEWORD(2, 2), &m_wWsaData) != 0)
    {
        PrintError("WSAStartup()");
    }

    //创建套接字
    m_hServSock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (INVALID_SOCKET == m_hServSock)
    {
        PrintError("socket()");
    }

    //填充sockaddr_in结构体
    memset(&m_sServAddr, 0, sizeof(m_sServAddr));
    m_sServAddr.sin_family = AF_INET;
    m_sServAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    m_sServAddr.sin_port = htons(8888);

    //分配IP地址和端口号
    if (bind(m_hServSock, (SOCKADDR*)&m_sServAddr, sizeof(m_sServAddr)) == SOCKET_ERROR)
    {
        PrintError("bind()");
    }
}

SocketServer::~SocketServer()
{
    closesocket(m_hServSock);
    closesocket(m_hClntSock);
    WSACleanup();  //终止前注销初始化的套接字库
}

void SocketServer::Listen()
{
    //端口监听
    if (listen(m_hServSock, SOMAXCONN) == SOCKET_ERROR)
    {
        PrintError("listen()");
    }
}

void SocketServer::Accept()
{
    m_iClntAddr = sizeof(m_sClntAddr);

    //受理连接请求
    m_hClntSock = accept(m_hServSock, (struct sockaddr*)&m_sClntAddr, &m_iClntAddr);
    if (SOCKET_ERROR == m_hClntSock)
    {
        PrintError("accept()");
    }

    //输出客户端的IP地址和端口号
    std::cout << "ClientIp:" << inet_ntoa(m_sClntAddr.sin_addr)
        << " Port:" << ntohs(m_sClntAddr.sin_port) << std::endl;
}

void SocketServer::SendMsg(const char* message)
{
    strcpy(m_sMessage, message);

    //向连接的用户传输数据
    send(m_hClntSock, m_sMessage, sizeof(m_sMessage) + sizeof(char), 0);
}

void SocketServer::RecvMsg()
{
    //接收数据
    recv(m_hClntSock, m_sMessage, sizeof(m_sMessage) + sizeof(char), 0);
    std::cout << "Message from client:" << m_sMessage << std::endl;
}

void SocketServer::PrintError(const char* err) const
{
    std::cerr << err << " error!" << std::endl;
    std::cin.get();
    exit(1);  //退出
} 

使用:

#include <iostream>
#include "SocketServer.h"

int main()
{
    char msg[] = "Hello Client";
    SocketServer sockServ;
    sockServ.Listen();
    sockServ.Accept();
    sockServ.SendMsg(msg);
    sockServ.RecvMsg();

    return 0;
}

客户端:

// SocketClnt.h
#include <iostream>
#include <Winsock2.h>
#pragma comment(lib, "ws2_32")

class SocketClnt
{
public:
    SocketClnt();
    ~SocketClnt();
    void Connect();
    void SendMsg(const char *message);
    void RecvMsg();

private:
    void PrintError(const char *err) const;

private:
    WSADATA m_wWsaData;
    SOCKET m_hSocket;
    SOCKADDR_IN m_sServAddr;
    char m_sMessage[MAXBYTE];
};

// SocketClnt.cpp
#include "SocketClnt.h"
#pragma warning(disable:4996)

SocketClnt::SocketClnt()
{
    //初始化套接字
    if (WSAStartup(MAKEWORD(2, 2), &m_wWsaData) != 0)
    {
        PrintError("WSAStartup()");
    }

    //创建套接字
    m_hSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (INVALID_SOCKET == m_hSocket)
    {
        PrintError("socket()");
    }

    memset(&m_sServAddr, 0, sizeof(m_sServAddr));
    m_sServAddr.sin_family = AF_INET;
    m_sServAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
    m_sServAddr.sin_port = htons(8888);
}

SocketClnt::~SocketClnt()
{
    closesocket(m_hSocket);
    WSACleanup();
}

void SocketClnt::Connect()
{
    //通过套接字向服务端发出链接请求
    if (connect(m_hSocket, (sockaddr*)&m_sServAddr, sizeof(m_sServAddr)))
    {
        PrintError("connect()");
    }
}

void SocketClnt::SendMsg(const char *message)
{
    strcpy(m_sMessage, message);

    //发送数据到服务端
    send(m_hSocket, m_sMessage, sizeof(m_sMessage) + sizeof(char), 0);
}

void SocketClnt::RecvMsg()
{
    //接受服务端发来的数据
    recv(m_hSocket, m_sMessage, sizeof(m_sMessage) + sizeof(char), 0);
    std::cout << "Message from server:" << m_sMessage << std::endl;
}

void SocketClnt::PrintError(const char* err) const
{
    std::cerr << err << " error!" << std::endl;
    std::cin.get();
    exit(1);
}

使用:

#include <iostream>
#include "SocketClnt.h"

int main()
{
    char msg[] = "Hello Server";
    SocketClnt sockClnt;
    sockClnt.Connect();
    sockClnt.RecvMsg();
    sockClnt.SendMsg(msg);

    return 0;
}

运行如下:


image
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容