socket编程

网络应用程序设计模式

C/S (client/server)客户端/服务器(数据量大,稳定性好)

优点:协议选用灵活、QQ、LOL、先下载客户端、数据可提前缓存
缺点:对用户安全造成威胁、开发工作量大

B/S(浏览器/服务器模式)(数据量小)

优点:使用计算机自带浏览器,较安全,只需开发服务器,客户端采用计算机自带浏览器
缺点:协议不可变http、QQ农场、可以跨平台(windows\linux)

socket基本概念

客户端(client)

或称为用户端,是指与服务器相对应,为客户提供本地服务的程序。产品或服务所指的终点、软件。

服务器(server)

对客户端发送的请求做出处理并将结果返回给客户端

端口号和IP

  • Socket成对出现,数据接收端、数据发送端
  • 端口号:在主机中唯一标识一个进程
  • IP:在网络环境中唯一标识一台主机
  • 端口号+IP:在网络环境中唯一标识一个进程(socket)
  • 使用socket必须确认IP,端口号

网络字节序

网络中的数据以二进制字节码流传输

存储方式

  • 大端:低地址存在高位上,高地址存在低位
  • 小端:低地址存在低位上,高地址存在高位上(计算机)

字节序转换

网络数据流应采用大端字节序,计算机选用的是小端存储,当计算机传数据时,从小端到大端,所以要进行字节序转换。

库函数(数字转网络字节序)
  • htonl(host to network long):主机到网络IP32位
  • ntohl(network to host long):网络到主机IP32位
    #include <arpa/inet.h>
    uint32_t htonl(uint32_t hostlong);//32位IP,从主机到网络
    uint16_t htons(uint16_t hostshort);//16位端口号 
    uint32_t ntohl(uint32_t netlong);//从网络到主机
    uint16_t ntohs(uint16_t netshort)
IP地址转换函数(字符串转网络字节序)
  • 点分十进制字符串转换成网络字节序 inet_pton(IP to network)
  • 网络字节序转换成点分十进制字符串 inet_ntop(network to IP)
    #include <arpa/inet.h>
    //af指定所用IP地址的形式(IPV4:AF_INET,IPV6:AF_INET6)
    //src指定源地址IP
    //dst指定目的地址IP
    int inet_pton(int af, const char *src, void *dst);
    const char *inet_ntop(int af,const void *src, char *dst, socklen_t size);

socket编程的实现

socket框架

服务器

  1. socket() // 建立套接字
  2. 对套接字进行初始化
  3. bind() //绑定端口号和IP(struct sockaddr_in addr)
  4. listen() //指定最大同时发起数
  5. accept() //阻塞等待客户端发起连接
  6. read() //读取客户端发送来的信息(对应客户端第4步)
  7. 对客户端信息进行处理
  8. Write() //将处理好的信息发送给客户端
  9. Close(); //关闭套接字

客户端

  1. socket();
  2. bind();//可以隐式绑定
  3. connect();//发起连接
  4. write();//写发送给服务器的信息
  5. read(); //读从服务器返回的信息(对应服务器端第7步)
  6. close();

注:程序启动时先启动服务器端,由服务器执行到第4步阻塞等待客户端发送信息,再启动客户端

客户端与服务器通信实现

  • 初始化缓冲区char buf[]
  • 通过缓冲区buf传信息
  • 在服务器端对buf进行清零操作
  • 每个write()buf重新赋值,read()对应另一方的上一个write()
  • 客户端write()buf写信息
  • 服务器read()读客户端buf信息

框架函数实现

头文件

#include <sys/types.h>
#include<sys/socket.h>

socket()

创建套接字

int socket(int domain, int type, int protocol);//创建套接口
  • Domain:函数socket()的参数domain用于设置网络通信的域,函数socket()根据这个参数选择通信协议的族。通信协议族在文件sys/socket.h中定义
  • Type: 函数socket()的参数type用于设置套接字通信的类型,有SOCK_STREAM(流式套接字)、SOCK_DGRAM(数据包套接字)
  • Protocol: 函数socket()的第3个参数protocol用于制定某个协议的特定类型,即type类型中的某个类型。通常某协议中只有一种特定类型,这样protocol参数仅能设置为0;但是有些协议有多种特定的类型,就需要设置这个参数来选择特定的类型。

初始化套接字

//套接字数据结构
Struct sockaddr_in{
_kernel_sa_family_t sin_family;
_be16 sin_port;
Struct in addr sin_addr;
}
//初始化
Struct sockaddr_in addr;
Addr.sin_family =AF_NET/NET6;
Addr.sin_port =htops/ntops;
Addr.sin_addr.s_addr =htops;

bind()

对套接字绑定端口号、IP,需要进行类型强转

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • Sockfd:socket文件描述符
  • Addr:构造出IP地址加端口号(1000以下的端口给系统用,不能大于65535)
  • Addrlen:sizeof(addr)长度
  • 返回值:成功返回0,失败返回-1

listen()

同时可以有多少个客户端与服务器建立连接,对套接字进行操作

int listen(int sockfd, int backlog);//backlog最大值为128

accept()

阻塞等待客户端发起连接,需进行类型强转

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

connect()

需进行类型强转

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

socket编程实例

客户端向服务器发送请求,服务器向客户端返回当前时间,并记录访问客户端的IP号、端口号

服务器端

#include <netdb.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <signal.h>
#include <memory.h>
#include <ctype.h>

int sockfd;//全局套接字描述符
char IP[16];
char *str;
//控制ctrl c产生的信号量处理函数
void signal_handler(int signno){
    if(signno== SIGINT){
       printf("服务器关闭\n");
       close(sockfd);
       exit(1);
    }
}

//输出客户端相关信息
char * out_addr(struct sockaddr_in *client_addr){
     //转换客户端port字节序
     int port = htons(client_addr->sin_port);
     memset(IP,0,sizeof(IP));
     //将ip地址从网络字节序转换成点十分进制
     inet_ntop(AF_INET,&client_addr->sin_addr.s_addr,IP, sizeof(IP));
     printf("用户:%s(%d) 已连接\n",IP,port);
     return IP;
}

//获取时间
char * do_service(int cfd){
     long t = time(0);//获得系统时间
     str = ctime(&t);
     size_t size = strlen(str) * sizeof(char);
     //将时间写入客户端sockaddr中
     if(write(cfd,str,size)!=size){
        printf("写入出错");
     }
     return str;
}



int main(int argc,char *argv[]){
        //在命令行中传递端口号
    if(argc <2){
       printf("usage:%s #port\n",argv[0]);
       exit(1);
    }

    //当客户端按下ctrl c时服务器端自动结束,否则服务器会一直循环
    //设置信号量进行控制
    if(signal(SIGINT, signal_handler)== SIG_ERR){
        perror("信号量错误");
        exit(1);
    }
        //创建文件
    FILE *f;
    //创建套接字,SOCK_STREAM使用TCP
    sockfd = socket(AF_INET,SOCK_STREAM,0);

    //在绑定函数之前对套接字进行初始化操作
    struct sockaddr_in server_addr;//套接字地址
    struct sockaddr_in client_addr;
    memset(&server_addr,0,sizeof(server_addr));//对套接字地址进行清空操作
    server_addr.sin_family =AF_INET;//与套接字协议保持一致,IPV4
    server_addr.sin_port = htons(atoi(argv[1]));//将端口号转换字节序,命令行传入的第二个参数为端口号,atoi将其强制转换为整形
    server_addr.sin_addr.s_addr = INADDR_ANY;//响应所有接口的链接请求


    //绑定端口号、IP
    int bindno;//用于错误处理
    bindno = bind(sockfd,(struct sockaddr *)&server_addr, sizeof(server_addr));
        if(bindno < 0){
       perror("绑定出错");
       exit(1);
    }

    //在指定端口进行监听
    int listenno;//错误处理
    listenno = listen(sockfd,128);//最多同时允许128个客户端请求建立链接
        if(listenno <0){
       perror("监听出错");
       exit(1);
    }

    //调用accept函数阻塞直到等待客户端请求连接
    socklen_t client_addr_len = sizeof(client_addr);//定义客户端套接字长度
    while(1){
         int cfd;//客户端文件描述符
         cfd = accept(sockfd, (struct sockaddr *)&client_addr, &client_addr_len);
         if(cfd <0){
            perror("接收失败");
        continue;//继续获得
         }

         //read函数读取客户端信息
         out_addr(&client_addr);//获取客户端地址信息
         do_service(cfd);//对客户端文件进行相关操作
         f= fopen("client_IP.txt","a+");
         fprintf(f,"用户IP:%s,请求时间:%s",IP,str);
         fclose(f);
             close(cfd);//关闭客户端套接字
        }
    fclose(f);
    return 0;
}

客户端

#include <stdlib.h>
#include <stdio.h>
#include <netdb.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <memory.h>
#include <unistd.h>

int main(int argc, char *argv[]){
    if(argc<3){
      printf("用户:%s IP #port\n",argv[0]);
      exit(1);
    }

    //创建套接字
    int sockfd = socket(AF_INET,SOCK_STREAM,0);
    if(sockfd<0){
       perror("套接字出错");
       exit(1);
    }
    struct sockaddr_in server_addr;
    memset(&server_addr,0,sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(atoi(argv[2]));//转换字节序
    //将IP地址转换成网络字节序
    inet_pton(AF_INET,argv[1],&server_addr.sin_addr.s_addr);

    //调用connect请求与服务器建立连接
    int a;
    a = connect(sockfd,(struct sockaddr *)&server_addr, sizeof(server_addr));
    if(a<0){
      perror("连接出错");
      exit(1);
    }

        //调用IO函数与服务器进行通信
    //提供一个缓冲区给服务器放置时间
    char buf[1024];
    memset(&buf,0,sizeof(buf));//清空缓冲区
    size_t size;
    size = read(sockfd, buf,sizeof(buf));
    if(size<0){
        perror("读取错误");
        exit(1);
    }
    //将日期写在屏幕上
    write(STDOUT_FILENO,buf,size);
        //关闭套接字
    close(sockfd);
    return 0;
}

   转载规则


《socket编程》 fightingtree 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录