TCP/UDP套接字编程

套接字编程:socket编程

一、概述
TCP(传输控制协议)和UDP(用户数据报协议是网络体系结TCP/IP模型中传输层一层中的两个不同的通信协议。
TCP:传输控制协议,一种面向连接的协议,给用户进程提供可靠的全双工的字节流,TCP套接口是字节流套接口(stream socket)的一种。
UDP:用户数据报协议。UDP是一种无连接协议。UDP套接口是数据报套接口(datagram socket)的一种。
二、基于udp协议的socket客户端与服务端通信编程:

aa1 int socket(int domain,int type,int protocol);//创建套接字
int bind(int sockfd,const struct sockaddr *addr,socklen_t *addrlen); //为套接字绑定地址信息
ssize_t recvfrom(int sockfd,void *buf,size_t len,int flags,sockaddr *src_addr,socklen_t *addrlen);
ssize_t sendto(int sockfd,void *buf,size_t len,int flags,struct sockaddr *dest_addr,socklen_t len);
int close(int fd); //关闭套接字
服务端: 1.创建套接字 在内核中创建struct socket结构体,使进程与网卡之间建立联系
int socket(domain,type,proto)
domain: 地址域 AF_INET–IPV4 地址域
type: 套接字类型
SOCK_STREAM 流式套接字
SOCK_DGRAM 数据报套接字
proto:传输层协议类型
0-默认
IPPROTO_TCP 6
IPPRPTO_UDP 17
返回值:套接字操作句柄–文件描述符
2.为套接字绑定地址信息
int bind(int socket, const struct sockaddr *address,socklen_t address_len);
在socket结构体中描述符这个socket处理哪个地址和端口的数据
服务端绑定地址的目的:告诉操作系统网卡接收到数据的时候,哪个地址和端口的数据应该放到这个socket的接收队列中
bind绑定只能绑定本机存在的地址
客户端:2.为套接字绑定地址信息;发送数据的时候能够表述数据从哪个地址端口发送出去,对方回复数据就会回复到这个地址端口上,
3.发送数据 sendto(sockfd,data,dlen,flag,dest_addr,addr_len)
将data中的dlen长度的数据通过sockfd对应的socket结构中的ip/端口将数据发送到dest_addr地址的主机上
4.接收数据
recvfrom(sockfd,buf,len,flag,&peer_addr,&addr_len)
从sockfd对应的socket结构体中的接收队列中取出一条数据放到buf中;

使用c++封装一个udpsocket类,来实现socket的简单操作

class UdpSocket{
private:
int_sockfd;
public:
bool Socket();
bool Bind(const std::string &ip,const uint16_t port);
bool Send(const std::string &data,const std::string &peer_ip,const uint16_tpeer_port);
bool Recv(std::string &buf,std::string &peer_ip,uint16_t &peer_port);
void Close();
}
UDP类的封装:

/*=============================================================== 
*   Copyright (C) . All rights reserved.")
*   文件名称: 
*   创 建 者:zhang
*   创建日期:
*   描    述:封装UdpSocket类,实例化对象,向外提供简单的socket接口 
*       1. 创建套接字
*       2. 为套接字绑定地址信息
*       3. 发送数据
*       4. 接收数据
*       5. 关闭套接字
================================================================*/

#include 
#include 
#include 
#include 
#include 
#include 

#define CHECK_RET(q) if((q)==false){return -1;}

class UdpSocket{
    private:
        int _sockfd;
    public: 
        bool Socket() {
            //int socket(int domain, int type, int protocol);
            _sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
            if (_sockfd < 0) {
                std::cerr << "socket error\n";
                return false;
            }
            return true;
        }
        bool Bind(const std::string &ip, const uint16_t port) {
            //bind(int sockfd, struct sockaddr *addr,socklen_t addrlen)
            struct sockaddr_in addr;
            addr.sin_family = AF_INET;
            //uint16_t htons(uint16_t hostshort);
            //将主机字节序的16位数据,转换位网络字节序数据返回
            addr.sin_port = htons(9000);
            //192.168.122.132 -> 0xc0a87a84
            //in_addr_t inet_addr(const char *cp);
            //将点分十进制字符串IP地址转换为网络字节序IP地址
            addr.sin_addr.s_addr = inet_addr(ip.c_str());
            int ret;
            socklen_t len = sizeof(struct sockaddr_in);
            ret = bind(_sockfd, (struct sockaddr*)&addr, len);
            if (ret < 0) {
                std::cerr << "bind error\n";
                return false;
            }
            return true;
        }
        bool Send(const std::string &data, const std::string &peer_ip, 
                const uint16_t peer_port) {
            //ssize_t sendto(int sockfd, const void *buf, size_t len, 
            //int flags,struct sockaddr *dest_addr, socklen_t addrlen);
            struct sockaddr_in addr;
            addr.sin_family = AF_INET;
            addr.sin_port = htons(peer_port);
            addr.sin_addr.s_addr = inet_addr(peer_ip.c_str());
            socklen_t len = sizeof(struct sockaddr_in);
            int ret = sendto(_sockfd, &data[0], data.size(), 0,
                    (struct sockaddr*) &addr, len);
            if (ret < 0) {
                std::cerr << "sendto error\n";
                return false;
            }
            return true;
        }
        bool Recv(std::string &buf, std::string &peer_ip, 
                uint16_t &peer_port) {
            //ssize_t recvfrom(int sockfd, void *buf, size_t len, 
            //int flags,struct sockaddr *src_addr, socklen_t *addrlen)
            //成功:返回实际接收的数据长度 , 失败:-1
            struct sockaddr_in peer_addr;
            socklen_t len = sizeof(struct sockaddr_in);
            char tmp[4096] = {0};
            int ret = recvfrom(_sockfd, tmp, 4096, 0, 
                    (struct sockaddr*)&peer_addr, &len);
            if (ret < 0) {
                std::cerr << "recvfrom error\n";
                return false;
            }
            //char *inet_ntoa(struct in_addr in);
            //将网络字节序IP地址转换为点分十进制字符串IP地址
            //uint16_t ntohs(uint16_t netshort);
            //将网络字节序的16位数据转换为主机字节序数据
            peer_ip = inet_ntoa(peer_addr.sin_addr);
            peer_port = ntohs(peer_addr.sin_port);
            buf.assign(tmp, ret);
            return true;
        }
        void Close() {
            close(_sockfd);
        }
};

UDP服务端代码:

#include "udpsocket.hpp"
#include 

int main(int argc, char *argv[]) 
{
    if (argc != 3) {
        std::cerr << "./udp_srv 192.168.122.132 9000\n";
        return -1;
    }
    uint16_t port;
    std::string ip = argv[1];
    std::stringstream tmp;
    tmp << argv[2];
    tmp >> port;

    UdpSocket sock;
    CHECK_RET(sock.Socket());
    CHECK_RET(sock.Bind(ip, port));

    while(1) {
        std::string buf;
        std::string peer_ip;
        uint16_t peer_port;
        sock.Recv(buf, peer_ip, peer_port);
        std::cout << "client-["<> buf;
        sock.Send(buf, peer_ip, peer_port);
    }
    sock.Close();
}

UDP客户端代码:

#include "udpsocket.hpp"
#include 

int main(int argc, char *argv[])
{
    if (argc != 3) {
        std::cerr << "./udp_cli ip port\n";
        return -1;
    }
    uint16_t port;
    std::string ip = argv[1];
    std::stringstream tmp;
    tmp << argv[2];
    tmp >> port;

    UdpSocket sock;
    CHECK_RET(sock.Socket());
    //客户端不推荐用户主动绑定固定地址,因为一个端口只能被一个进程占用
    //因此一旦端口固定,这个客户端程序就只能启动一个
    while(1) {
        std::string buf;
        std::cin >> buf;
        //当socket还没有绑定地址,这时候操作系统在发送之前可以检测到
        //这时候操作系统会为socket选择一个合适的地址和端口进行绑定
        sock.Send(buf, ip, port);

        buf.clear();
        sock.Recv(buf, ip, port);
        std::cout << "server say:" << buf << std::endl;
    }
    sock.Close();
    return 0;
}

三、基于tcp协议的客户端与服务端通讯流程:面向连接,可靠传输,面向字节流
客户端: 服务端:
tcp客户端/服务端通信流程实现

class TcpSocket
{
    private:
            int _sockfd;
    public:
            bool Socket();
            bool Bind(std::string &ip,std::string &port);
            bool Listen(int backlog=5);
            bool Connect(std::string &srv_ip,std::string &srv_port);
            bool Accept(TcpSocket &clisock,std::string *ip=NULL,uint16_t *port=NULL);
            bool Send(std::string &data);
            bool Recv(std::string &buf);
            bool Close();
};

当前服务端程序只能于一个客户端通信一次;
原因:因为服务端不知道客户端的新连接请求/数据什么时候到来,因此在程序流程写死的情况,就会阻塞在recv或者accpt两个接口处,导致流程无法继续
解决方案:服务端为每一个新的客户端都创建一个进程/线程来与客户端进行通信

连接断开的体现:当通信双方连接断开时
recv返回0(recv默认没有数据则阻塞)–表示连接断开,应该关闭套接字
send触发异常—SIGPIPE信号,会导致进程退出
tcp服务端缺陷:
tcp服务端为每个客户端都新建了套接字进行独立通信,但是服务端无法获知哪个客户端数据会先到来,因此可能会阻塞在等待连接请求或者等待接收某个客户端数据这里。
解决方案: 多线程/多进程任务处理
每个线程/进程独立负责一个功能
一个线程/进程复制客户端已完成连接获取功能
为每个客户端都新建一个线程/进程处理独立通信。
实现代码:
TCP类的封装:

/*=============================================================== 
*   Copyright (C) . All rights reserved.")
*   文件名称: 
*   创 建 者:zhang
*   创建日期:
*   描    述:z封装一个tcpsocket类,向外提供简单的套接字接口 
*       1. 创建套接字
*       2. 为套接字绑定地址信息
*       3. 开始监听
*       4. 向服务端发起连接请求
*       5. 服务端获取新建连接
*       6. 发送数据
*       7. 接收数据
*       8. 关闭套接字
================================================================*/
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define CHECK_RET(q) if((q)==false){return -1;}

class TcpSocket
{
    private:
        int _sockfd;
    public:
        void SetFd(int fd) {
            _sockfd = fd;
        }
        int GetFd() {
            return _sockfd;
        }
        bool Socket() {
            _sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
            if (_sockfd < 0) {
                std::cerr << "socket error\n";
                return false;
            }
            return true;
        }
        int str2int(const std::string &str){
            int num;
            std::stringstream tmp;
            tmp << str;
            tmp >> num;
            return num;
        }
        bool Bind(const std::string &ip, const std::string &port) {
            struct sockaddr_in addr;
            addr.sin_family = AF_INET;
            addr.sin_port = htons(str2int(port));
            addr.sin_addr.s_addr = inet_addr(ip.c_str());
            socklen_t len = sizeof(struct sockaddr_in);
            int ret = bind(_sockfd, (struct sockaddr*)&addr, len);
            if (ret < 0) {
                std::cerr << "bind error\n";
                return false;
            }
            return true;
        }
        bool Listen(const int backlog = 5) {
            //int listen(int sockfd, int backlog);
            //开始监听:通知操作系统,可以开始接收客户端的连接请求了,
            //并且完成三次握手建立连接过程
            //tcp的面向连接,有一个三次握手建立连接过程
            //backlog:客户端最大并发连接数(同一时间最多接收多少个客户端
            //新连接请求)
            int ret = listen(_sockfd, backlog);
            if (ret < 0) {
                std::cerr << "listen error\n";
                return false;
            }
            return true;
        }
        bool Connect(const std::string &srv_ip, 
                const std::string &srv_port) {
            //int connect(int sockfd, sockaddr *addr,socklen_t addrlen)
            //addr: 服务端地址信息
            //addrlen:  地址信息长度
            struct sockaddr_in addr;
            addr.sin_family = AF_INET;
            addr.sin_port = htons(str2int(srv_port));
            addr.sin_addr.s_addr = inet_addr(srv_ip.c_str());
            socklen_t len = sizeof(struct sockaddr_in);
            int ret = connect(_sockfd, (struct sockaddr*)&addr, len);
            if (ret < 0) {
                std::cerr << "connect error\n";
                return false;
            }
            return true;
        }
        bool Accept(TcpSocket &clisock, std::string *ip = NULL, 
                uint16_t *port = NULL) {
            //int accept(int sockfd, sockaddr *addr, socklen_t *addrlen)
            //sockfd:   监听套接字描述符
            //addr:    客户端地址信息
            //addrlen: 地址信息长度
            //返回值:返回新建连接的socket描述符-与客户端进行数据通信
            struct sockaddr_in cliaddr;
            socklen_t len = sizeof(struct sockaddr_in);
            int newfd = accept(_sockfd, (sockaddr*)&cliaddr, &len);
            if (newfd < 0) {
                std::cerr << "accept error\n";
                return false;
            }
            clisock.SetFd(newfd);
            if (ip != NULL) {
                *ip = inet_ntoa(cliaddr.sin_addr);
            }
            if (port != NULL) {
                *port = ntohs(cliaddr.sin_port);
            }
            return true;
        }
        bool Send(std::string &data) {
            //ssize_t send(int sockfd, void *buf, size_t len, int flags)
            //sockfd: 套接字描述符(服务端是新建连接的socket描述符)
            //buf: 要发送的数据
            //len: 要发送的数据长度
            //flags:   0-默认阻塞发送
            //返回值: 成功-返回实际发送的数据长度;失败-返回-1
            int ret = send(_sockfd, &data[0], data.size(), 0);
            if (ret < 0) {
                std::cerr << "send error\n";
                return false;
            }
            return true;
        }
        bool Recv(std::string &buf) {
            //ssize_t recv(int sockfd, void *buf, size_t len, int flags)
            //flags:
            //  0-默认阻塞接收
            //  MSG_PEEK:从缓冲区取数据,但是数据并不从缓冲区移除
            //返回值:>0:实际接收的数据长度 ==0:连接断开   <0:错误
            char tmp[4096];
            int ret = recv(_sockfd, tmp, 4096, 0);
            if (ret < 0) {
                std::cerr << "recv error\n";
                return false;
            }else if (ret == 0) {
                std::cerr << "connect shutdown\n";
                return false;
            }
            buf.assign(tmp, ret);
            return true;
        }
        bool Close() {
            close(_sockfd);
        }
};

TCP服务端代码:

/*=============================================================== 
 *   Copyright (C) . All rights reserved.")
 *   文件名称: 
 *   创 建 者:zhang
 *   创建日期:
 *   描    述:tcp服务端通信流程
 *       1. 创建套接字
 *       2. 为套接字绑定地址信息
 *       3. 开始监听
 *       4. 获取已完成连接socket
 *       5. 通过获取的新建socket与客户端进行通信-接收数据
 *       6. 发送数据
 *       7. 关闭套接字 
 ================================================================*/
#include 
#include "tcpsocket.hpp"
int main(int argc, char *argv[])
{
    if (argc != 3) {
        std::cerr << "./tcp_srv ip port\n";
        return -1;
    }
    TcpSocket lst_sock;
    /*1. 创建套接字*/
    CHECK_RET(lst_sock.Socket());
    /*2. 为套接字绑定地址信息*/
    CHECK_RET(lst_sock.Bind(argv[1], argv[2]));
    /*3. 开始监听*/
    CHECK_RET(lst_sock.Listen());
    while(1){
        /*4. 获取已完成连接socket*/
        TcpSocket clisock;
        bool ret = lst_sock.Accept(clisock);
        if (ret == false) {
            continue;
        }
        /*5. 通过获取的新建socket与客户端进行通信-接收数据*/
        std::string buf;
        ret = clisock.Recv(buf);
        if (ret == false) {
            clisock.Close();
            continue;
        }
        std::cout << "client say: " << buf << std::endl;
        /*6. 发送数据*/
        buf.clear();
        std::cout << "server say: ";
        fflush(stdout);
        std::cin >> buf;
        clisock.Send(buf);
    }
    /*7. 关闭套接字 */
    lst_sock.Close();
    return 0;
}


TCP客户端代码:

/*=============================================================== 
*   Copyright (C) . All rights reserved.")
*   文件名称: 
*   创 建 者:zhang
*   创建日期:
*   描    述:tcp客户端通信流程
*       1. 创建套接字
*       2. 为套接字绑定地址信息(不推荐用户主动绑定)
*       3. 向服务端发起连接请求
*       4. 发送数据
*       5. 接收数据
*       6. 关闭套接字 
================================================================*/
#include 
#include 
#include "tcpsocket.hpp"
void sigcb(int signo)
{
    printf("recv a signo SIGPIPE --- conect shutdown\n");
}
int main(int argc, char *argv[])
{
    if (argc != 3) {
        std::cerr << "./tcp_cli ip port\n";
        return -1;
    }
    signal(SIGPIPE, sigcb);
    TcpSocket sock;
    /*1. 创建套接字*/
    CHECK_RET(sock.Socket());
    /*2. 为套接字绑定地址信息(不推荐用户主动绑定)*/
    /*3. 向服务端发起连接请求*/
    CHECK_RET(sock.Connect(argv[1], argv[2]));
    while(1) {
        /*4. 发送数据*/
        std::string buf;
        std::cout << "client say: ";
        fflush(stdout);
        std::cin >> buf;
        sock.Send(buf);
        /*5. 接收数据*/
        buf.clear();
        sock.Recv(buf);
        std::cout << "server say: " << buf << std::endl;
    }
    /*6. 关闭套接字 */
    sock.Close();
    return 0;
}

TCP进程实现:

/*=============================================================== 
 *   Copyright (C) . All rights reserved.")
 *   文件名称: 
 *   创 建 者:zhang
 *   创建日期:
 *   描    述:tcp服务端通信流程
 *       1. 创建套接字
 *       2. 为套接字绑定地址信息
 *       3. 开始监听
 *       4. 获取已完成连接socket
 *       5. 通过获取的新建socket与客户端进行通信-接收数据
 *       6. 发送数据
 *       7. 关闭套接字 
 ================================================================*/
#include 
#include 
#include 
#include "tcpsocket.hpp"

void sigcb(int no) {
    //如果有僵尸进程可以处理,就一直处理
    //如果没有子进程退出了则waitpid返回0,退出循环
    while(waitpid(-1, NULL, WNOHANG) > 0);
}
int main(int argc, char *argv[])
{
    if (argc != 3) {
        std::cerr << "./tcp_srv ip port\n";
        return -1;
    }
    signal(SIGCHLD, sigcb);
    TcpSocket lst_sock;
    /*1. 创建套接字*/
    CHECK_RET(lst_sock.Socket());
    /*2. 为套接字绑定地址信息*/
    CHECK_RET(lst_sock.Bind(argv[1], argv[2]));
    /*3. 开始监听*/
    CHECK_RET(lst_sock.Listen());
    while(1){
        /*4. 获取已完成连接socket*/
        TcpSocket clisock;
        bool ret = lst_sock.Accept(clisock);
        if (ret == false) {
            continue;
        }
        if (fork() == 0) {
            while(1) {
                /*5. 通过获取的新建socket与客户端进行通信-接收数据*/
                std::string buf;
                clisock.Recv(buf);
                std::cout << "client say: " << buf << std::endl;
                /*6. 发送数据*/
                buf.clear();
                std::cout << "server say: ";
                fflush(stdout);
                std::cin >> buf;
                clisock.Send(buf);
            }
            clisock.Close();
        }
        clisock.Close();
    }
    /*7. 关闭套接字 */
    lst_sock.Close();
    return 0;
}


四、对比tcp/udp协议,对比两者优缺点。
TCP优缺点:
优点:
1.TCP提供以认可的方式显式地创建和终止连接。
2.TCP保证可靠的、顺序的(数据包以发送的顺序接收)以及不会重复的数据传输。
3.TCP处理流控制。
4.允许数据优先
5.如果数据没有传送到,则TCP套接口返回一个出错状态条件。
6.TCP通过保持连续并将数据块分成更小的分片来处理大数据块。—无需程序员知道
缺点: TCP在转移数据时必须创建(并保持)一个连接。这个连接给通信进程增加了开销,让它比UDP速度要慢。
UDP优缺点:
1.UDP不要求保持一个连接
2.UDP没有因接收方认可收到数据包(或者当数据包没有正确抵达而自动重传)而带来的开销。
3.设计UDP的目的是用于短应用和控制消息
4.在一个数据包连接一个数据包的基础上,UDP要求的网络带宽比TDP更小。

你可能感兴趣的:(TCP/UDP套接字编程)