Linux网络编程之UDP广播、组播与简单Socket

本篇文章主要介绍下UDP广播、组播以及简单Socket的实现(顺带在说下CGI)

UDP广播和UDP组播(组播可实现跨网段通讯)

UDP广播只能在同一网段(严格意义上的网段)内进行广播,不可跨网段广播,而UDP组播可以实现跨网段广播,组播地址只能作为目的地址(组播地址也就是组播组),另外组播也被称为多播
组播架构:点对点两台主机,或者多台主机连接配置了支持组播的路由器(路由一般默认关闭组播功能)。这些主机IP地址都可不在同一网段

IGMP协议:
IGMP是IP组播的基础,在IP协议出现以后为了加入对组播的支持,IGMP产生了。IGMP所做的时间上就是告诉路由,在这个路由所在的子网内有人对发送到某一个组播组(组播IP)的数据感兴趣,这样当这个组播组的数据到达后,路由就不会抛弃它,而是把它转送给所有感兴趣的客户。假如不同子网内的A和B要进行组播通讯,那么位于AB直接的所有路由器必须都支持IGMP协议,否则AB之间不能进行通讯

组播的原理:
组播首先有一个用户申请一个组播组,这个组播组被维护在路由中,其他用户申请加入,这样当一个用户向组内发送消息时,路由器将消息转发给组内的所有成员。如果申请加入的组不在本级路由中,且路由和交换机允许组播协议通过,路由则将申请加入的操作箱上级路由提交。广域网通讯要经过多级路由器和交换机,几乎所有的网络设备都默认阻止组播协议通过(只允许本网段内的广播,不向上级提交),这使得广域网组播实现有一定局限

UDP组播基本步骤:
1、建立socket
2、socket和端口绑定(最初的Receive方需要bind 0.0.0.0端口)
3、加入一个组播组
4、通过sendto/recvfrom进行数据收发
5、关闭socket
所有终端必须都要加入相同的主播地址

Python组播实现:
Terminal_One:

#-*- coding:utf-8 -*-

import time
import struct
from socket import *

SENDERIP = '192.168.8.11' # 本地ip
SENDERPORT = 1501  # 本地接口
MYPORT = 1234  # 发送数据到该端口
MYGROUP = '225.0.0.77'  # 组播组
MYTTL = 255  # 发送数据的TTL


def sender():
    s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
    s.bind((SENDERIP, SENDERPORT))
    # Set Time-to-live (optional)
    ttl_bin = struct.pack('@i', MYTTL)
    s.setsockopt(IPPROTO_IP, IP_MULTICAST_TTL, ttl_bin)
    status = s.setsockopt(IPPROTO_IP,
                          IP_ADD_MEMBERSHIP,
                          inet_aton(MYGROUP) + inet_aton(SENDERIP))  # 加入到组播组
    i = 0
    while True:
        data = 'cisco'
        s.sendto(data + '\0', (MYGROUP, MYPORT))
        i = i + 1
        print "%d send data ok !" % i
        time.sleep(10)


if __name__ == "__main__":
    sender()

Terminal_Two:

# -*- coding:utf-8 -*-

import time
import socket

SENDERIP = '169.254.51.246'
MYPORT = 1234 #监听组播组端口
MYGROUP = '225.0.0.77'

def receiver():
    # create a UDP socket
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
    # allow multiple sockets to use the same PORT number
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    # Bind to the port that we know will receive multicast data
    #bind一般用于服务器绑定、监听本地IP和端口,只可以可以绑定本机所具有的IP和端口
    #但在组播中bind应该设置为监听组播IP和其端口,但bind无法设置为绑定、监听非本机的IP(保留IP也不可以被设置为监听对象)
    #所以在组播中必须bind所有ip,和对应组播的端口号
    sock.bind(('0.0.0.0', MYPORT))#留空也可以
    # tell the kernel that we are a multicast socket
    # sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 255)
    # Tell the kernel that we want to add ourselves to a multicast group
    # The address for the multicast group is the third param
    #加入组播组
    status = sock.setsockopt(socket.IPPROTO_IP,
                             socket.IP_ADD_MEMBERSHIP,
                             socket.inet_aton(MYGROUP) + socket.inet_aton(SENDERIP));

    sock.setblocking(0)
    # ts = time.time()
    while 1:
        try:
            data, addr = sock.recvfrom(1024)
        except socket.error, e:
            pass
        else:
            print "Receive data!"

            print "TIME:", time.time()
            print "FROM: ", addr
            print "DATA: ", data


if __name__ == "__main__":
    receiver()  

CGI程序:

CGI程序一般指运行于HTTP服务器上的后台交互程序
Web后台架构可分为:
1、服务端(HTTP协议解析)
2、中间件(WSGI才有)
3、应用端(CGI/WSGI程序)
一个HTTP请求到达一台服务器的80端口后,需要有一个程序来响应该请求。所谓HTTP Response,其实只是运行一个程序,它的输入是HTTP Request Header,它的返回是HTTP Response。HTTP Request Header传递方式就分为CGI、WSGI以及其他各种GI的区别
如果是CGI,通常来说是一个Web Server(例如Apache、Nginx)接收到请求后,将请求中的HTTP Request Header按照一定规则设置成环境变量,然后启动一个程序,将stdout的输出(其中HTTP Response Header)封装成HTTP Response返回给客户的
例如:Django跑在uWSGI、unicorn之类的容器里,那么程序是一个常驻进程,Web Server和Python进程用WSGI协议传递HTTP Request Header信息,然后返回给用户。如果是Django的dev server,它使用Python自带的wsgiref模块实现了一个简单的HTTP Server响应HTTP请求。从以上可以知道服务端Server对利用Apache等软件对HTTP协议解析后还会将其解析的信息加入另一协议中,此协议根据服务器的后台逻辑框架而定(Django用WSGI协议),利用此进行服务端HTTP请求和后台逻辑框架的交互

Socket

Socket是应用层与TCP/IP协议族通信的中间软件抽象层。注意,UDP则不具备这一软件抽象层
Socket在网络通信中所处的位置:

Socket网络编程常用结构体(struct sockaddr, struct in_addr, struct sockaddr_in)定义:

struct sockaddr
{
    unsigned short sa_family;//协议族(定义使用哪种底层网络协议),AF_INET(TCP/IPv4),AF_INET6(TCP/IPv6),AF_LOCAL或AF_UNIX(本地通信,用于本地进程间的socket通讯)
    char sa_data[14];//14字节的协议地址
}  

//str 2 in_addr变量赋值需使用inet_addr函数:in_addr addr = inet_addr("192.168.0.2");
//in_addr变量 2 str需使用inet_ntoa函数:char *str = inet_ntoa(addr);
typedef struct in_addr
{
    union{
        struct{unsigned char s_b1, s_b2, s_b3, s_b4;}S_un_b;
        struct{unsigned short s_w1, s_w2;}S_un_w;
        unsigned long S_addr;
    }S_un;
}IN_ADDR; 

struct sockaddr_in
{
    short int sin_family;//协议族,在网络编程中一般就是用AF_INET
    unsigned short int sin_port;//存放端口号(需转为大端模式再存放)
    struct in_addr sin_addr;//存储IP地址
    unsigned char sin_zero[8];//留空字节,保证sockaddr和sockaddr_in可以相互转换
}  

Socket网络通讯编程流程:
Server端:
1、创建套接字(Socket)
2、将套接字绑定到一个本地地址和端口上(Bind)
3、将套接字设置为监听模式,准备接收客户端请求(Listen)
4、等待客户端请求到来,当请求到来后接受连接请求,返回一个新的对应于此次连接请求的套接字(accept)
5、用返回的套接字和客户端进行通信(Send/Recv)
6、返回,等待新请求
7、关闭套接字
一个简单的Server端Socket实现(C语言):

/*************************************************************************
    > File Name: simplesocket.c
    > Author:lizhong 
    > Mail: 
    > Created Time: Thu 09 Mar 2017 11:02:12 PM PST
 ************************************************************************/

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>//路径:/usr/include/
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>

#define DPORT 33333
#define SIZE 1000

int Select(int confd)
{
    fd_set fd;
    struct timeval time;
    int ts  = 0;

    time.tv_sec = 0;//0秒
    time.tv_usec = 500;//500毫秒

    FD_ZERO(&fd);
    FD_SET(confd, &fd);

    /*最后一个time参数设置为空意味着阻塞,直到有收到东西*/
    ts = select(confd + 1, &fd, NULL, NULL, NULL);

    printf("Select Return:%d\n", ts);

    if(ts)
        return 1;
    else
        return 0;
}


int main()
{
    int sfd,confd;
    struct sockaddr_in serveradd;
    char *rebuff;
    char *sebuff;//= "I received the message!";
    int relen = 0,j = 0,k = 0, sel = 0;
    /*初始化Socket,返回socket的文件描述符*/
    sfd = socket(AF_INET, SOCK_STREAM, 0);

    if( sfd == -1 )
    {
        printf("Created Socket Error:%s\n", strerror(errno));
        exit(0);
    }
    /*配置本服务器地址参数*/
    memset(&serveradd, 0, sizeof(struct sockaddr_in));
    serveradd.sin_family = AF_INET;
    /*系统自动获取本机IP*/
    serveradd.sin_addr.s_addr = htonl(INADDR_ANY);
    /*设置监听端口为DPORT*/
    serveradd.sin_port = htons(DPORT);

    /*Socket和端口绑定*/
    j = bind(sfd, (struct sockaddr*)&serveradd, sizeof(struct sockaddr_in));

    if(j == -1)
    {
        printf("Bind Error :%s\n", strerror(errno));
        exit(0);
    }
    j = 0;
    /*开启监听客户端请求,(开闸)*/
    j = listen(sfd, 10);

    if(j == -1)
    {
        printf("Listen Error:%s\n", strerror(errno));
        exit(0);
    }
    j= 0;
    printf("***********************Wait Request**********************\n");

    /*接受客户端连接,此条语句有阻塞效果*/
     confd = accept(sfd, (struct sockaddr*)NULL, NULL);

    if(confd == -1)
    {
        printf("Accept Error:%s\n", strerror(errno));
        /*出错则结束*/
        return 0;
    }
    while(1)
    {
        /*接收客户端传来的数据*/
        rebuff = malloc(SIZE);
        memset(rebuff, 0, SIZE);

        /*注意传入confd,而不是sfd。阻塞,直到有收到东西,收到东西之后用recv函数接收下数据*/
        /*将select函数放到recv函数后将一直返回0,因为recv后缓冲区没有数据了*/
        sel = Select(confd);

        /*flags设置为0值则是阻塞的(默认阻塞),并且接受完一次数据后接收缓冲区的数据会被清除*/
        /*因为先前socket设置了socket stream(使用面向连接的TCP协议)所以没有数据被丢失,具体表现为:*/
        /*SIZE过小会触发多次接收,每次relen的值最大为SIZE*/
        relen = recv(confd, rebuff, SIZE, 0);

        if(relen != -1 || relen != 0)
        {
            /*可以接收数据,但是数据长度却为0说明客户端断开了TCP连接*/
            if(sel == 1 && relen == 0)
            {
                printf("Socket is close!\n");
                free(rebuff);
                 break;
            }
            else
            {
                printf("Receive Data is :\n********%d:%s****relen:%d********\n", ++k, rebuff, relen);
                sebuff = malloc(strlen("I received the message:") + relen);
                strcpy(sebuff, "I received the message:");//'\0'也会被copy(MSDN上有说明)
                strcat(sebuff, rebuff);
                send(confd, sebuff, strlen(sebuff), 0);
                free(rebuff);
                free(sebuff);
            }
        }
        else
        {
            printf("Receive Error is :%s\n", strerror(errno));
            free(rebuff);
            break;
        }
    }
    return 0;
}  

Client端:
1、创建套接字(Socket)
2、向服务端发出连接请求(Connect)
3、和服务端进行通信(Send/Recv)
4、关闭套接字
Client与Server流程图:

Socket编程中的一些坑: