在了解网络编程相关接口之前,我们要知道网络字节序

我们都知道,内存中的多字节数据相对于内存地址有大小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大小端之分,网络数据流同样也有大小端之分,那么如何定义网络数据流的地址呢?

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
  • 接收缓冲区把从网络上接收到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
  • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址;
  • TCP/IP协议规定,网络数据流应采用打断字节序,即低地址高字节。不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
  • 如果当前发送主机是小端机,就需要先将数据转成大端,否则就忽略,直接发送即可;

为了使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。

#include<arpa/inet.h>

uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

这些函数名中,h表示host,n表示network,l表示32位长整数,s表示16位短整数。例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。如果主机是小端字节序,这些函数将参数做相应的大小端转换后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动返回

socket常见的API

1.int socket(int domain,int type,int protocol);

头文件:

#include<sys/types.h>

#include<sys/socket.h>

解释:

  • soket()打开一个网络通讯端口,如果成功返回一个文件描述符。
  • 应用程序可以像读写文件一样用read和write在网络上收发数据
  • 如果socket()调用出错则返回-1
  • 对于IPv4,family参数指定为AF_INET
  • 对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议
  • protocol参数默认指定为0

2.int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);

头文件

#include<sys/types.h>

#include<sys/socket.h>

解释:

  • 服务器程序所监听的网络地址和端口号通常都是固定不变的,客户端程序得知服务器的地址和端口号后就可以向服务器发起连接,服务器需要调用bind绑定一个固定的网络地址和端口号。
  • bind()成功返回0,失败返-1.
  • bind()的作用是将参数sockfd和myaddr绑定在一起,使sockfd这个网络通讯的文件描述符监听myaddr所描述的地址和端口号

我们在程序中对myaddr参数要进行初始化:

bzero(&myaddr,sizeof(myaddr));//将整个结构体清零
myaddr.sin_family = AF_INET;//设置地址类型为AF_INET
myaddr.sin_addr.s_addr = htonl(INADDR_ANY);//网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底该用哪个IP地址
myaddr.sin_port = htons(SERV_PORT);//端口号为SERV_PORT,我们定义为9999

 

这里我们需要了解sockaddr结构。

虽然socket api的接口是sockaddr,但是在基于IPv4编程时,使用的数据结构是sockaddr_in,这个结构里面主要有三部分的信息:地址类型,端口号,IP地址。

3.int listen(int sockfd,int backlog);

头文件

#include<sys/types.h>

#include<sys/socket.h>

解释:

  • listen()声明sockfd处于监听状态,并且最多允许有backlog个客户处于等待连接状态,如果接收到更多的连接请求就忽略,这里设置不会太大,一般为5个
  • listen()成功返回0,失败返回-1

4.int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);

头文件

#include<sys/types.h>

#include<sys/socket.h>

解释:

  • 三次握手完成后,服务器调用accept()接受连接
  • 如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到客户端连接上来
  • addr是一个传出参数,accept()返回时传出客户端的地址和端口;如果给addr参数一个NULL,表示不关心客户端的地址
  • addrlen参数是一个传入传出参数,传入的是调用者提供的缓冲区addr的长度,以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)

5.int connect(int sockfd,const struct sockaddr * addr,socklen_t addrlen);

头文件

#include<sys/types.h>

#include<sys/socket.h>

解释:

  • 客户端需要调用connect()连接服务器
  • connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址
  • connect成功返回0,出错返回-1

地址转换函数

基于IPv4的socket网络编程,sockaddr_in成员中的成员struct in_addr sin_addr表示32位的IP地址,但是我们通常都是用点分十进制的字符串表示IP地址,以下函数可以在字符串和in_addr表示之间转换。

字符串转in_addr的函数

#include<arpa/inet.h>

int inet_aton(const char* strptr,struct in_addr* addrptr);
in_addr_t inet_addr(const char* strptr);
int inet_pton(int family,const char* strptr,void* addrptr);

in_addr转字符串的函数(常用)

char* inet_ntoa(struct in_addr inaddr);
const char* inet_ntop(int family,const void* addrptr,char* strptr,size_t len);

下面我们详细说一下inet_ntoa函数

inet_ntoa这个函数返回一个一个char*,很显然是这个函数自己在内部为我们申请了一块内存来保存IP的结果,那么是否需要自己手动释放呢。查看man手册上说,inet_ntoa函数是把这个返回结果放到了静态存储区,这个时候不需要我们手动进行释放。如果我们多调用几次这个函数

运行结果如下:

因为inet_ntoa把结果放到自己内部的一个静态存储区,这样二次调用时的结果会覆盖掉上一次的结果。

所以如果在多个线程中调用inet_ntoa函数可能会出现异常安全,在APUE(Unix环境高级编程)一书中明确提出inet_ntoa不是线程安全的函数。但是在Centos7上进行测试时,并没有出现问题,可能是内部实现了互斥锁。

在多线程环境下,推荐使用inet_ntop,这个函数由调用者提供一个缓冲区保存结果,可以规避线程安全问题。

认识TCP、UDP协议

关于TCP、UDP协议详细细节请看博客:传输层中的“端口号”和UDP、TCP协议

实现一个简单的UDP、TCP协议的客户端/服务器网络程序

UDP协议的客户端/服务器网络程序

udpServer.cc

#include <iostream>
#include <stdlib.h>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>

using namespace std;

void Usage(string proc)
{
    cerr << "IP PORT" << endl;
}

int main(int argc,char* argv[])
{
    if(argc != 3){
        Usage(argv[0]);
        return 1;
    }
    int sock = socket(AF_INET,SOCK_DGRAM,0);
    if(sock < 0){
        cerr << "socket error" << endl;
        return 2;
    }
    cout << "sock: " << sock << endl;
    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_port = htons(atoi(argv[2]));
    local.sin_addr.s_addr = inet_addr(argv[1]);
    
    if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0){
        cerr << "bind error" << endl;
        return 3;
    }

    char buf[1024];
    for(;;){
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        ssize_t s = recvfrom(sock,buf,sizeof(buf)-1,0,(struct sockaddr*)&peer,&len);
        if(s > 0){
            buf[s] = 0;
            cout << "client# " << buf << endl;
            sendto(sock,buf,strlen(buf),0,(struct sockaddr*)&peer,len);
        }
    }
    close(sock);
    return 0;
}

udpClient.cc

#include <iostream>
#include <stdlib.h>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>

using namespace std;
void Usage(string proc)
{
    cerr << proc << "server_ip server_port" << endl;
}

int main(int argc,char* argv[])
{
    if(argc != 3){
        Usage(argv[0]);
        return 1;
    }
    int sock = socket(AF_INET,SOCK_DGRAM,0);
    if(sock < 0){
        cerr << "socket error" << endl;
        return 2;
    }
    cout << "sock: " << sock << endl;
    struct sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_port = htons(atoi(argv[2]));
    server.sin_addr.s_addr = inet_addr(argv[1]);

    char buf[1024];
    for(;;){
        cout << "Please Enter# ";
        cin >> buf;
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        sendto(sock,buf,strlen(buf),0,(struct sockaddr*)&server,sizeof(server));
        ssize_t s = recvfrom(sock,buf,sizeof(buf)-1,0,(struct sockaddr*)&peer,&len);
        if(s > 0){
            buf[s] = 0;
            cout << "server echo# " << buf << endl;
            sendto(sock,buf,strlen(buf),0,(struct sockaddr*)&peer,len);
        }
    }
    close(sock);
    return 0;
}

Makefile

.PHONY:all
all:udpClient udpServer
udpClient:udpClient.cc
	g++ -o $@ $^
udpServer:udpServer.cc
	g++ -o $@ $^
.PHONY:clean
clean:
	rm -f udpClient udpServer

TCP协议的客户端/服务器网络程序

tcpServer.hpp

#pragma once

#include <iostream>
#include <cstring>
#include <string>
#include <strings.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>

class tcpServer
{
private:
    int listen_sock;
    int port;
public:
    tcpServer(int port_)
        :port(port_)
        ,listen_sock(-1)
    {}
    void InitServer()
    {
        listen_sock = socket(AF_INET,SOCK_STREAM,0);
        if(listen_sock < 0){
            std::cerr << "socket error" << std::endl;
            exit(2);
        }
        //使用setsockopt函数允许创建端口号相同但IP地址不同的多个socket描述符
        int opt = 1;
        setsockopt(listen_sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
        
        struct sockaddr_in local;
        bzero(&local,sizeof(local));//结构体清零
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        //local.sin_addr.s_addr = inet_addr(ip.c_str());
        local.sin_addr.s_addr = htonl(INADDR_ANY);//不用指定IP地址,只要是给服务器指定的端口号发送的连接,服务器都能接收
        if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local)) < 0){
            std::cerr << "bind error" << std::endl;
            exit(3);
        }
        if(listen(listen_sock,5) < 0){//将套接字设置为监听状态
            std::cerr << "listen error" << std::endl;
            exit(4);
        }
        signal(SIGCHLD,SIG_IGN);
    }
    void Service(int sock,std::string ip_,int port_)
    {
        while(1){
            char buf[1024];
            ssize_t s = read(sock,buf,sizeof(buf)-1);//从sock文件描述符中读出数据放到buf所指的内存中
            if(s > 0){
                buf[s] = 0;//将最后一个字符设置为0
                std::cout << "[" << ip_ << ":" << port_ << "]" << buf << std::endl;
                write(sock,buf,strlen(buf));//将buf中的数据写进sock文件描述符中
            }else if(s == 0){//读到文件结尾,说明客户端退出了,不仅没有往套接字中写数据,还将套接字关闭了
                std::cout << "client quit!" << std::endl;
                break;
            }else{
                std::cerr << "read error" << std::endl;
                break;
            }
        }
        close(sock);
    }
    void Start()
    {
        struct sockaddr_in peer;//存储建立连接的相关数据
        socklen_t len;
        for(;;){
            len = sizeof(peer);
            int sock = accept(listen_sock,(struct sockaddr*)&peer,&len);//接收建立好的连接,并开始服务
            if(sock < 0){
                std::cerr << "accept error" << std::endl;
                continue;//失败后,继续重新访问
            }
            std::cout << "get a new client" << sock << std::endl;
            //获取客户端信息
            int port_ = ntohs(peer.sin_port);
            std::string ip_ = inet_ntoa(peer.sin_addr);

            //创建多线程,让服务器可以连接多个客户端
            pid_t id = fork();
            if(id < 0){
                std::cerr << "fork error" << std::endl;
                close(sock);
                continue;
            }else if(id == 0){
            //子进程创建成功,让子进程进行服务
                close(listen_sock);
                Service(sock,ip_,port_);
                exit(0);
            }else{
            //父进程继续accept,这里主要的问题是父进程是否等子进程?是阻塞式等待还是非阻塞式等待?
            //如果父进程不等待子进程,子进程退出资源无法回收,子进程就会变成僵尸进程;
            //如果父进程等待子进程,并采用阻塞等待时,若第一批接收的连接不退出则父进程无法接收其他客户端的发起的连接。
            //如果采用非阻塞式等待,若再也没有客户端发起连接时,父进程就会阻塞等待接收连接,从而子进程又没有被父进程等待变成僵尸进程。
            //这里我们采用的解决办法就是使用SIGCHLD信号,这样子进程不会默默退出,子进程退出时会给父进程发送SIGCHLD信号。
            //父进程只要忽略该信号,不进行处理,操作系统就会自动回收子进程的资源,就不会造成僵尸进程了。
            close(sock);
            }
        }
    }
    ~tcpServer()
    {
        if(listen_sock > 0){
            close(listen_sock);
        }
    }
};

tcpServer.cc

#include "tcpServer.hpp"

using namespace std;

void Usage(string proc)
{
    cout << "Usage: " << proc << "port" << std::endl;
}
int main(int argc,char* argv[])
{
    if(argc != 2){
        Usage(argv[0]);
        exit(1);
    }

    int port = atoi(argv[1]);
    tcpServer* ts = new tcpServer(port);
    ts->InitServer();
    ts->Start();
    return 0;
}

tcpClient.hpp

#pragma once

#include <iostream>
#include <cstring>
#include <stdlib.h>
#include <string>
#include <strings.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>

class tcpClient
{
private:
    int sock;
    std::string ip;
    int port;
public:
    tcpClient(std::string ip_,int port_)
        :ip(ip_)
        ,port(port_)
        ,sock(-1)
    {}
    void InitClient()
    {
        sock = socket(AF_INET,SOCK_STREAM,0);//创建套接字
        if(sock < 0){
        //如果套接字创建出错,打印出错信息
            std::cerr << "socket error" << std::endl;
            exit(2);
        }
    }
    void Start()
    {
       struct sockaddr_in peer;//该结构体用以存储与服务器建立连接的数据
       bzero(&peer,sizeof(peer));//将结构体清零
       peer.sin_family = AF_INET;
       peer.sin_port = htons(port);//使用htons将本地端口号转换为网络字节序的端口号
       peer.sin_addr.s_addr = inet_addr(ip.c_str());//将IP地址转换成字符串形式跨网络传输
       //与服务器建立连接
       if(connect(sock,(struct sockaddr*)&peer,sizeof(peer)) < 0){
           std::cerr << "connect error" << std::endl;
           exit(3);
       }
       //建立成功后,客户端向服务器发送请求
       char buf[1024];
       std::string in;
       while(1){
        std::cout << "Please Enter# ";
        std::cin >> in;
        write(sock,in.c_str(),in.size());
        ssize_t s = read(sock,buf,sizeof(buf));
        if(s > 0){
            buf[s] = 0;
            std::cout << "server echo# " << buf << std::endl;
        }else if(s == 0){
            std::cout << "server don't have meaasge" << std::endl;
        }else{
            std::cout << "read error" << std::endl;
            exit(4);
        }
      }
    }
    ~tcpClient()
    {
        if(sock > 0)
            close(sock);
    }
};

tcpClient.cc

#include "tcpClient.hpp"

using namespace std;

void Usage(string proc)
{
    cout << "Usage: " << proc << "server_ip server_port"<< endl;
}

int main(int argc,char* argv[])
{
    if(argc != 3){
        Usage(argv[0]);
        exit(1);
    }
    int port = atoi(argv[2]);
    string ip = argv[1];
    tcpClient* tc = new tcpClient(ip,port);
    tc->InitClient();
    tc->Start();
    return 0;
}

Makefile

.PHONY:all
all:tcpClient tcpServer
tcpClient:tcpClient.cc
	g++ -o $@ $^
tcpServer:tcpServer.cc
	g++ -o $@ $^
.PHONY:clean
clean:
	rm -f tcpClient tcpServer

 

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐