网络编程2(套接字编程)

Posted 沉默....后....的...爆发.

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了网络编程2(套接字编程)相关的知识,希望对你有一定的参考价值。

套接字编程

套接字编程:如何编写一个网络通信程序
1.网络通信的数据中都会包含一个完整的五元组:
sip,sport,dip,dport,protocol(源IP,源端口,对端IP,对端端口,协议)
五元组完整的描述了数据从哪来,到哪去,用什么数据格式
2.网络通信–两个主机进程之间的通信:客户端&服务端
客户端:用户使用,发起请求
服务端:网络应用提供商提供服务的程序(后台开发)

UDP协议通信:

服务端一方要提前启动,保证有数据到来时,一定能够接收;并且服务端永远都是先接收数据,因为服务端这一方并没有保存客户端的地址,没有地址绑定,也就没有数据来源,也不知道数据要发送什么,要发给谁
客户端:创建套接字,端口绑定(不推荐),发送数据,接收数据,关闭套接字

操作接口:
socket–创建套接字

domain—地址域类型(域间通信、IPv4通信、IPv6通信)AF_INET—IPv4网络协议

type–套接字类型
SOCK_STREAM ; SOCK_DGRAM


bind–为套接字绑定地址信息
int bind(int sockfd, struct sockaddr *addr, socklen_t address_len)
三个参数:套接字描述符,要绑定的地址(不同地址域,有不同的地址结构),sockaddr结构体的长度


sendto:发送数据
参数:sockfd—返回的套接字描述符
buf–要发送的数据空间起始地址
len–要发送的数据长度(从buf地址开始,发送len长度的数据)
flags–默认0-阻塞发送(发送缓冲区满了就等着)
dest_addr–对端地址信息,数据要发送给谁,目的地
addrlen–对端地址信息长度


recvfrom:接收数据
对于recvfrom的src_addr参数,是为了获取数据是谁发送的,相当于用指针接收返回值,addrlen也一样(可以看到这里的addrlen是指针)

int close(int fd)
关闭套接字,释放资源

字节序相关接口:

htonl(32位),htons(16位),ntohl,ntohs ; l–32位 ; s–16位
这几个接口已经进行了主机字节序的判断,因此无需担心自己的主机字节序
32位数据转换接口与16位不能混用,会出现数据截断,就像汉字用两个字节表示,而如果按字节输出就会乱码,并且再大小端转换时,数据截断会造成数据的错误


sockaddr结构体


udp_srv.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>//字节序转换接口头文件
#include <netinet/in.h>//地址结构类型以及协议类型宏头文件
#include <sys/socket.h>//套接字接口头文件

int main(int agrc, char *argv[])

    if (agrc != 3)
    
        printf("/unp_srv 192.168.2.2 9000\\n");
        return -1;
    

    uint16_t port = atoi(argv[2]); // 输入的参数都是按字符串存的,这里将端口port转int
    char *ip = argv[1];

    // 创建套接字  int socket(int domain, int type ,int protocol)
    int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); // ipv4域,数据报,udp协议
    if (socket < 0)
    
        perror("socket error");
        return -1;
    

    // 为套接字绑定地址信息  int bind(int sockfd, struct sockaddr* addr,socklen_t len)
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port); // 16位,存储用16位解析也用16位
    addr.sin_addr.s_addr = inet_addr(ip);
    socklen_t len = sizeof(struct sockaddr_in);
    int ret = bind(sockfd, (struct sockaddr *)&addr, len);
    if (ret == -1)
    
        perror("bind error");
        return -1;
    

    // 循环接收发送数据
    while (1)
    
        char buf[1024] = 0;
        struct sockaddr_in peer; // 地址由系统设置,数据谁发的,设置的就是谁
        socklen_t len = sizeof(struct sockaddr_in);
        ssize_t ret = recvfrom(sockfd, buf, 1023, 0, (struct sockaddr *)&peer, &len);
        if (ret < 0)
        
            perror("recvfrom error");
            return -1;
        

        // const char* inet_ntoa(struct in_addr addr);
        char *peerip = inet_ntoa(peer.sin_addr); // 转成字符串
        uint16_t peerport = ntohs(peer.sin_port);
        printf("client[%s:%d] say: %s\\n", peerip, peerport, buf);

        // 发送数据
        // ssize_t sendto(int sockfd, void* buf, int len, int flag, struct sockaddr* peer, socklen_t len)
        char data[1024] = 0;
        printf("server say:");
        fflush(stdout);
        scanf("%s", data);
        ret = sendto(sockfd, data, strlen(data), 0, (struct sockaddr *)&peer, len);
        if (ret < 0)
        
            perror("sendto error");
            return -1;
        

        
    
    // 关闭套接字
        close(sockfd);
    return 0;



g++ -std=c++11 -o udp_cli udp_cli.cpp
udp_cli.cpp

#include "udp_socket.hpp"
int main(int argc, char *argv[])

    if (argc != 3)
    
        std::cout << "usage: ./udp_cli 192.168.2.2 9000\\n";
        return -1;
    
    std::string srv_ip = argv[1];
    uint16_t srv_port = std::stoi(argv[2]);

    UdpSocket cli_sock;
    assert(cli_sock.Socket()==true);

    while (1)
    
        std::string data;
        std::cout << "clinet say: ";
        fflush(stdout);
        std::cin >> data;
        assert(cli_sock.Send(data, srv_ip, srv_port)==true);

        data.clear();
        assert(cli_sock.Recv(&data)==true);
        std::cout << "server say:" << data << std::endl;
    

    cli_sock.Close();
    return 0;

udp_socket.hpp //封装socket接口

#include <iostream>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <cstdio>
#include <cassert>
#include <string>
class UdpSocket

private:
    int _sockfd;

public:
    UdpSocket():_sockfd(-1)
    ~UdpSocket()Close();
    bool Socket()
    
        _sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
        if (_sockfd < 0)
        
            perror("socket error");
            return false;
        
        return true;
    
    bool Bind(const std::string &ip, uint16_t port)
    
        struct sockaddr_in addr;
        addr.sin_family = AF_INET;
        addr.sin_port = htons(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)
        
            perror("bind error");
            return false;
        
        return true;
    
    bool Recv(std::string *body, std::string *peer_ip = NULL, uint16_t *peer_port = NULL)
    
        // ssize_t recvfrom(int _fd, void *_restrict_ _buf, size_t _n, int _flags, sockaddr *_restrict_ _addr, socklen_t *_restrict_ _addr_len)
        struct sockaddr_in peer;
        socklen_t len = sizeof(struct sockaddr_in);
        char tmp[4096] = 0;
        ssize_t ret = recvfrom(_sockfd, tmp, 4096, 0, (struct sockaddr *)&peer, &len);
        if (ret < 0)
        
            perror("recvfrom error");
            return false;
        
        if(peer_ip!=NULL) *peer_ip = inet_ntoa(peer.sin_addr);
        if(peer_port!=NULL) *peer_port = ntohs(peer.sin_port);
        body->assign(tmp, ret); // 从tmp中取出ret长度的数据,放到body
        return true;
    

    bool Send(const std::string &body, const std::string &peer_ip, uint16_t peer_port)
    
        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);
        // ssize_t sendto(int __fd, const void *__buf, size_t __n, int __flags, const sockaddr *__addr, socklen_t __addr_len)
        ssize_t ret = sendto(_sockfd, body.c_str(), body.size(), 0, (struct sockaddr *)&addr, len);
        if (ret < 0)
        
            perror("sendto error");
            return false;
        
        return true;
    
    bool Close()
    
        if (_sockfd != -1)
        
            close(_sockfd);
            _sockfd = -1;
        
        return true;
    
;

TCP通信:

客户端向服务器发送一个请求,服务段处于listen监听状态,则会对这个连接请求进行处理:
1.为这个新链接请求,创建一个套接字结构体socket
2.为这个新的socket,描述完整的五元组信息(sip,sport,dip,dport,protocol)
往后的数据通信都是由这个新的套接字进行通信
一个服务器上有多少客户端想要简历连接,服务端就要创建多少个套接字
最早服务端创建的监听套接字–只负责新连接请求处理,不负责数据通信
服务端会为每个客户端都创建一个新的套接字,负责与这个客户端进行数据通信,但是想要通过这个套接字与客户顿进行通信,就要拿到这个套接字的描述符sockfd



相较于UDP套接字通信,TCP通信多了listen,connect,accept,
用recv,send简化了数据收发,因为地址信息都在sockfd描述的socket结构体中五元组完全包含,并且TCP通信时有状态的status
recv
这些调用返回接收到的字节数,如果发生错误,则返回-1。在如果发生错误,则设置errno以指示错误。
返回值将为0,说明没有数据,实际是对方已经执行有序关闭时,连接断开了

在客户端要注意监听套接字listenfd,通信套接字connfd的区别,listenfd只负责连接的建立在listen和accept时使用,当要发送数据是用accept返回的套接字connfd进行recv数据发送

tcpsocket.hpp(封装系统调用接口)

#ifndef __M_TCP_H__
#define __M_TCP_H__
#include <iostream>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <cstdio>
#include <cassert>
#include <string>

#define MAX_LISTEN 1024
class TcpSocket

private:
    /* data */
    int _sockfd; // 这是监听套接字

public:
    TcpSocket() : _sockfd(-1) 
    ~TcpSocket()
    
        Close();
        _sockfd = -1;
    
    bool Socket()
    
        // int socket(int domain, int type, int protocol)
        _sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        if (_sockfd < 0)
        
            perror("socket error");
            return false;
        
        return true;
    
    bool Bind(const std::string &ip, uint16_t port)
    
        // int bind(int sockfd, struct sockaddr* addr, socklen_t len)
        struct sockaddr_in addr;
        addr.sin_family = AF_INET;
        addr.sin_port = htons(port);                  // port是2字节,要注意字节序问题,使用htons
        addr.sin_addr.s_addr = inet_addr(ip.c_str()); // 192.168.154.131 ===> 转换成网络字节序的整形ip
        // 客户端bind的sockaddr要进行内容填充,将协议族,ip,端口都要确定
        socklen_t addrlen = sizeof(struct sockaddr_in);
        // struct sockaddr: Structure describing a generic socket address.
        int ret = bind(_sockfd, (struct sockaddr *)&addr, addrlen);
        if (ret < 0)
        
            perror("bind error");
            return false;
        
        return true;
    
    bool Listen(int backlog = MAX_LISTEN)
    
        // int listen(int backlog)
        int ret = listen(_sockfd, backlog);
        if (ret < 0)
        
            perror("listen error");
            return false;
        
        return true;
    

    bool Accept(TcpSocket &newsock) // 这里传入的newsock引用的是外部定义的新socket对象,用来保存通信套接字connfd,accept系统函数传入的是listenfd
    
        // int accpet(int sockfd, struct sockaddr* peer,sock_len* len)
        int newfd = accept(_sockfd, (struct sockaddr *)NULL, NULL); // addr设置为NULL时,表示不关心客户端的地址,addrlen也应该设置为NULL
        if (newfd < 0)
        
            perror("accept error");
            return false;
        
        newsock._sockfd = newfd; // 将获取的新建连接描述符,赋值给外部传入的TcpSocket对象

        return true;
    

    bool Recv(TcpSocket &sock, std::string &body)
    
        // ssize_t recv(int sockfd, void* buf, int len, int flag);//收发数据的sockfd是accept获取的新建连接的描述符,不是监听Socket套接字
        char tmp[1024] = 0;
        // recv返回值Returns the number read or -1 for errors.为0时说明连接断开,所以也就没有数据
        ssize_t ret = recv(sock._sockfd, tmp, 1023, 0); // flag=0,表示默认阻塞接收(接受缓冲区没有数据就阻塞)
        if (ret < 0)
        
            perror("recv error");
            return false;
        
        else if (ret == 0)
        
            std::cout << "connect broken";
            return false;
        
        body.assign(tmp, ret); // 从temp中截取ret大小数据放到body这个string对象中
        return true;
    

    bool Send(TcpSocket &sock, const std::string &body)
    
        // ssize_t send(int sockfd, void* data, int len, int flag)
        ssize_t ret = send(sock._sockfd, body.c_str(), body.size(), 0);
        if (ret < 0)
        
            perror("send error");
            return false;
        
        return true;
    

    bool Connect(TcpSocket &sock, const std::string &ip, uint16_t port) // 客户端使用connect
    
        // int connect(int sockfd, struct sockaddr* srvaddr, socklen_t len);
        套接字介绍

1.套接字 : 实现网络编程进行数据传输的一种技术手段

2.Python实现套接字编程:import socket

3.套接字分类

  • 流式套接字(SOCK_STREAM): 以字节流方式传输数据,实现tcp网络传输方案。(面向连接--tcp协议--可靠的--流式套接字)
  • 数据报套接字(SOCK_DGRAM):以数据报形式传输数据,实现udp网络传输方案。(无连接--udp协议--不可靠--数据报套接字)

tcp套接字

服务端流程

技术图片

1.创建套接字

sockfd=socket.socket(socket_family=AF_INET,socket_type=SOCK_STREAM,proto=0)

  • 功能:创建套接字
  • 参数: socket_family 网络地址类型 AF_INET表示ipv4
  •     socket_type 套接字类型 SOCK_STREAM(流式) SOCK_DGRAM(数据报)
  •     proto 通常为0 选择子协议
  • 返回值: 套接字对象

2.绑定地址

本地地址 : ‘localhost‘ , ‘127.0.0.1‘
网络地址 : ‘172.40.91.185‘
自动获取地址: ‘0.0.0.0‘

sockfd.bind(addr)

  • 功能: 绑定本机网络地址
  • 参数: 二元元组 (ip,port) (‘0.0.0.0‘,8888)

技术图片

3.设置监听

sockfd.listen(n)

  • 功能 : 将套接字设置为监听套接字,确定监听队列大小
  • 参数 : 监听队列大小

4.等待处理客户端连接请求

connfd,addr = sockfd.accept()

  • 功能: 阻塞等待处理客户端请求
  • 返回值: connfd 客户端连接套接字
  •     addr 连接的客户端地址

5.消息收发

data = connfd.recv(buffersize)

  • 功能 : 接受客户端消息
  • 参数 :每次最多接收消息的大小
  • 返回值: 接收到的内容

n = connfd.send(data)

  • 功能 : 发送消息
  • 参数 :要发送的内容 bytes格式
  • 返回值: 发送的字节数

6.关闭套接字

sockfd.close()

  • 功能:关闭套接字
技术图片tcp服务端流程

客户端流程

技术图片

1.创建套接字

注意:只有相同类型的套接字才能进行通信

2.请求连接

sockfd.connect(server_addr)

  • 功能:连接服务器
  • 参数:元组 服务器地址

3.收发消息

注意: 防止两端都阻塞,recv send要配合

4.关闭套接字

技术图片
 1 """
 2 重点代码
 3 """
 4 
 5 from socket import *
 6 
 7 # 创建tcp套接字
 8 sockfd = socket() # 参数默认即tcp套接字
 9 
10 # 连接服务端程序
11 server_addr = ("172.40.91.150",8888)  # 服务端地址
12 sockfd.connect(server_addr)
13 
14 while True:
15   # 消息发送接收
16   data = input("Msg>>")
17   # 如果直接回车,则跳出循环
18   if not data:
19     break
20   sockfd.send(data.encode()) # 转换字节串发送
21   data = sockfd.recv(1024)
22   print("Server:",data.decode())
23 
24 sockfd.close()
tcp客户端流程

tcp 套接字数据传输特点

  • tcp连接中当一端退出,另一端如果阻塞在recv,此时recv会立即返回一个空字串。
  • tcp连接中如果一端已经不存在,仍然试图通过send发送则会产生BrokenPipeError
  • 一个监听套接字可以同时连接多个客户端,也能够重复被连接

网络收发缓冲区

  • 网络缓冲区有效的协调了消息的收发速度
  • send和recv实际是向缓冲区发送接收消息,当缓冲区不为空recv就不会阻塞。

tcp粘包

代码示例:day2/stick_send.py,stick_recv.py

原因:tcp以字节流方式传输,没有消息边界。多次发送的消息被一次接收,此时就会形成粘包。

影响:如果每次发送内容是一个独立的含义,需要接收端独立解析此时粘包会有影响。

处理方法:

  • 人为的添加消息边界
  • 控制发送速度
技术图片
 1 from socket import *
 2 
 3 
 4 sockfd = socket()
 5 
 6 server_addr = ("172.40.91.150",8888)
 7 sockfd.connect(server_addr)
 8 
 9 while True:
10   sockfd.send(bhello)
11 
12 sockfd.close()
tcp 粘包
技术图片
 1 import socket
 2 
 3 sockfd = socket.socket(socket.AF_INET,
 4                        socket.SOCK_STREAM)
 5 sockfd.bind((0.0.0.0, 8888))
 6 
 7 sockfd.listen(3)
 8 
 9 while True:
10   print("Waiting for connect ...")
11   connfd, addr = sockfd.accept()
12   print("Connect from", addr)
13 
14   n = 0
15   while n < 10:
16     n += 1
17     data = connfd.recv(5)
18     print(data)
19 
20   connfd.close()  # 断开连接
21 
22 # 关闭套接字
23 sockfd.close()
tcp粘包问题

练习:将一个文件从客户端发送到服务端,要求文件类型随意.
思路:读取文件--> send发送       recv接收--> write写入

技术图片
 1 from socket import *
 2 import time
 3 #    读取文件--> send发送
 4 s = socket()
 5 s.connect((127.0.0.1,8888))
 6 
 7 f = open(img.jpg,rb)
 8 
 9 # 读取内容,将其发送
10 while True:
11   data = f.read(1024)
12   if not data:
13     time.sleep(0.1)
14     s.send(b##)
15     break
16   s.send(data)
17 
18 time.sleep(0.1)
19 s.send("发送完毕".encode())
20 
21 f.close()
22 s.close()
23 ---------------------------------------------
24 from socket import *
25 
26 s = socket()
27 s.bind((0.0.0.0,8888))
28 s.listen(3)
29 
30 c,addr = s.accept()
31 print("Connect from",addr)
32 
33 # 以二进制写入
34 f = open(mm.jpg,wb)
35 
36 #循环接收内容,写入文件
37 while True:
38 #    recv接收--> write写入
39   data = c.recv(1024)
40   if data == b##:
41     break
42   f.write(data)
43 
44 data = c.recv(1024)
45 print(data.decode())
46 
47 f.close()
48 c.close()
49 s.close()
练习

 

以上是关于网络编程2(套接字编程)的主要内容,如果未能解决你的问题,请参考以下文章

计算机网络—网络原理之TCP/IP协议

Socket编程详解

Java网络编程:TCP的socket编程

Java网络编程:TCP的socket编程

Linux之socket套接字编程20160704

linux 网络编程