网络编程与套接字

推荐书籍 TCP/IP网络编程,是目前我读的最适合我的网络编程书籍。
此系列博客是我学习这本书的记录,算是代替纸张笔记了。
在这里插入图片描述

第一章:网络编程与套接字

1、1 概要:
  • 网络编程:编写程序使两台联网的计算机相互交换数据。

  • 套接字:是网络数据传输用的软件设备。连接因特网的工具。

  • 创建等待连接请求的服务器端。 服务器端套接字或监听套接字。

    • 由socket函数生成套接字。
    #include <sys/socket.h>
    // 调用 socket 函数创建套接字
    int socket(int domain, int type, int protocol); // 成功时返回文件描述符,失败时返回-1.
    
    • 给创建好的套接字分配地址信息(IP 地址端口号
    #include <sys/socket.h>
    //调用 bind 函数分配ip地址和端口号
    int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen); // 成功返回0,失败返回-1.
    
    • 将套接字转化为可接收连接的状态,之后就可以收到连接请求。
    #include <sys/socket.h>
    // 调用 listen 函数将套接字转为可接受连接状态
    int listen(int sockfd, int backlog); // 成功返回0,失败返回-1.
    
    • 接收对方的连接请求来完成数据传输。
    #include <sys/socket.h>
    //  accept函数受理连接请求,如果在没有连接请求的情况下调用该函数,则不会返回,直到有连接请求为止
    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);// 成功时返回文件描述符,失败返回-1
    
    • 等待连接请求的套接字创建过程总结:
      • 第一步:调用socket 函数创建套接字。
      • 第二步:调用bind函数分配 IP 地址和端口号。
      • 第三步:调用listen函数转为可接收请求状态。
      • 第四步:调用accept函数受理连接请求。
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/socket.h>
    #include <unistd.h>
    #include <arpa/inet.h>
    void error_handling(char*message);
    
    int main(int argc,char *argv[]){
    
        int serv_sock;
        int clnt_sock;
    
        // 定义在头文件 <arpa/inet.h>中,定义了Inernet socket address
        struct sockaddr_in serv_addr;
        struct sockaddr_in clnt_addr;
        socklen_t clnt_addr_size;
    
        char message[] = "Hello World!";
    
        if(argc != 2){
            printf("Usage : %s <port>\n",argv[0]);
            exit(1);
        }
    
        // PF_INET ,IP协议簇,#define PF_INET 2
        // SOCK_STREAM 有序的、可信赖的、基于连接的字节流
        // 调用socket函数生成 服务器socket套接字
        serv_sock = socket(PF_INET,SOCK_STREAM,0);
        // socket函数成功返回文件描述符,失败返回-1
        if(serv_sock == -1)
            error_handling("socket() error");
        
        // sin_family 端口号,#define AF_INET PF_INET
        memset(&serv_addr,0,sizeof(serv_addr));
        serv_addr.sin_family = AF_INET;
        serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
        serv_addr.sin_port = htons(atoi(argv[1]));
    
        // bind函数将创建好的套接字分配IP地址和端口号
        if(bind(serv_sock,(struct sockaddr*) &serv_addr,sizeof(serv_addr)) == -1)
            error_handling("listen() error");
        // listen函数将套接字转化为可接受连接状态
        if(listen(serv_sock,5) == -1)
            error_handling("listen() error");
        
        clnt_addr_size = sizeof(clnt_addr);
        // accept函数受理连接请求,如果在没有连接请求的情况下调用该函数,则不会返回,直到有连接请求为止
        clnt_sock= accept(serv_sock,(struct sockaddr*)&clnt_addr,&clnt_addr_size);
        if (clnt_sock == -1)
            error_handling("accept() error");
        //稍后要将介绍的 write 函数用于传输数据,若程序经过 accept 这一行执行到本行,则说明已经有了连接请求
        write(clnt_sock, message, sizeof(message));
        close(clnt_sock);
        close(serv_sock);
        return 0;
    }
    
    void error_handling(char *message)
    {
        // 将字符串写入 stderr 流中
        fputs(message, stderr);
        // 将字符写入erroe stream中
        fputc('\n', stderr);
        exit(1);
    }
    

请求连接的客户端套接字,(客户端套接字)

  • 请求连接函数 connect,调用connect函数向服务器端发送请求。

  • #include <sys/socket.h>
    int connect(int sockfd,struct sockaddr * serv_addr, socklen_t addrlen); // 成功返回0,失败返回-1
    
  • 请求连接的客户端套接字

    • 调用socket函数创建套接字
    • 调用connect函数向服务器端发送连接请求
  • #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <arpa/inet.h>
    #include <sys/socket.h>
    void error_handling(char *message);
    
    int main(int argc, char* argv[]){
        int sock;
        struct sockaddr_in serv_addr;
        char message[30];
        int str_len;
    
        if(argc!=3){
            printf("Usage : %s <IP> <port>\n",argv[0]);
            exit(1);
        }
        // 调用 socket函数创建套接字 ,此时套接字不马上分为服务端和客户端,若调用bind、listen函数,称为服务端套接字,调用connect函数,是客户端套接字
        sock = socket(PF_INET,SOCK_STREAM,0);
        if(sock == -1)
            error_handling("socket() error");
        
        memset(&serv_addr, 0, sizeof(serv_addr));
        serv_addr.sin_family = AF_INET;
        serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
        serv_addr.sin_port = htons(atoi(argv[2]));
    
        //调用 connect 函数向服务器发送连接请求
        if(connect(sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr)) == -1)
            error_handling("connect() error");
        
        str_len = read(sock,message,sizeof(message) -1);
        if(str_len == -1)
            error_handling("read() error");
        
        printf("Message from server : %s \n", message);
        close(sock);
        return 0;
        
    }
    
    void error_handling(char *message)
    {
        fputs(message, stderr);
        fputc('\n', stderr);
        exit(1);
    }
    
  • 编译运行:

    • 服务器端需要在运行时接受客户端连接请求,所有先编译运行服务器端,运行后程序会停留在此。

    • gcc hello_server.c -o hserver
      ./hserver 9190
      
    • 编译运行客户端

    • gcc hello_client.c -o hclient
      ./hclient 127.0.0.1 9190
      // 127.0.0.1 是服务器端IP地址
      
1、2 基于Linux的文件操作
  • Linux系统中,socket也是文件的一种,所有网络数据传输中需要使用到文件I\O的相关函数。

  • 底层文件访问(Low-Level File Access)和文件描述符(File Descriptor)

    • 这里的底层指与标准无关的操作系统独立提供的。

    • 文件描述符指系统分配给文件或套接字的整数。即操作系统创建文件或套接字赋予的数。又称为句柄(Windows平台),描述符(Linux平台)。

    • 文件描述符对象
      0标准输入:Standard Input
      1标准输出:standard Output
      2标准错误:Standard Error
  • 打开文件函数:open

    • 第一个参数是打开的目录文件名及路径信息,第二个参数是文件打开模式

    • #include <sys/types.h>
      #include <sys/stat.h>
      #include <fcntl.h>
      int open(const char * path, int flag);// 成功时返回文件描述符,失败返回-1
      
    • 打开模式含义
      O_CREAT必要时创建文件
      O_TRUNC删除全部现有数据
      O_APPEND维持现有数据,保存到其后面
      O_RDONLY只读打开
      O_WRONLY只写打开
      O_RDWR读写打开
    • 打开模式如果需要传入多个参数,通过位或运算(OR)组合传递。

  • 关闭文件函数:close

    • 使用文件后要关闭。参数fd指要关闭的文件或套接字的文件描述符。

    • #include <unistd.h>
      int close(int fd); // 成功时返回0, 失败时返回-1
      
    • 函数close不仅可以关闭文件还能关闭套接字。

  • 将数据写入文件/套接字:write函数

    • 向文件/套接字输出(传输)数据。

    • 参数 fd 显示数据传输对象的文件描述符

    • buf 保存要传输数据的缓冲地址值

    • nbytes 要传输数据的字节数

    • // 标准符号常量和类型头文件,是C和C++的可移植操作系统API的头文件名称
      #include <unistd.h>
      ssize_t write(int fd, const void * buf, size_t nbytes); // 成功时返回写入的字节数,失败返回-1
      
    • size_t 是 unsigned int 类型,ssize_t 是通过 typedef 声明的signed int 类型。

  • 读取文件中的数据:read函数

    • read函数用来读取数据

      • #include <unistd.h>
        // 成功时返回接收的字节数(遇到文件结尾返回0),失败时返回-1
        ssize_t read(int fd, void *buf, size_t nbytes);
        
      • fd显示数据接收对象的文件描述符

      • buf 要保存接收数据的缓冲地址值

      • nbytes 要接收数据的最大字节数。

  • 描述符从3开始由以小到大的顺序编号,因为0、1、2是分配给标准I/O的描述符。

1、3 基于Windows平台的实现:

  • 为Windows套接字编程设置头文件和库

    • 开发网络程序的准备工作:
      • 导入头文件winsock2.h
      • 链接ws2_32.lib库
        • 在项目属性中打开 “配置属性”-> “链接”-> “额外依赖”->“ws2_32.lib”;
  • Winsock的初始化

    • 进行Winsock编程时,需要调用WSAStartup函数,设置程序中用到的Winsock版本,初始化相应版本的库。

    • #include <winsock2.h>
      int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);// 成功时返回0,失败时返回非0的错误值代码
      
      • wVersionRequested 程序员要用的Winsock版本信息

        • Winsock存在多个版本,应准备WORD类型的套接字版本信息。(WORD是typedef 声明定义的unsigned short类型)并传递给WSAStartup函数的第一个参数wVersionRequested。

        • 版本1.2则1是主版本号,2是副版本号,传递给wVersionRequest的值是0x0201, 高8位是副版本号,低8位是主版本号。

        • 可以借助MAKEWORD宏函数构建WORD型版本信息。

        • MAKEWORD(1,2); // 主版本为1,副版本为2,返回0x0201
          MAKEWORD(2,2); // 主版本为2,副版本为2,返回0x0202 
          
      • lpWSAData : WSADATA结构体变量地址值

        • 参数lpWSAData中需要传入WSADATA结构体变量地址。(LPWSADATA是WSADATA的指针类型)

        • 调用完函数,相应参数中将添加已初始化的库信息,为了调用函数,必须传递WSADATA结构体变量地址。

        • int main(int argc, char * argv[]){
              WSADATA wsaData;
              ......
              if(WSAStartup(MAKEWORD(2,2),&wsaData)!= 0)
                  ErrorHanding("WSAStartup() error!");
              ......
              return 0;
          }
          
    • Winsock相关库注销方法:WSACleanup

      • #include <winsock2.h>
        int WSACleanup(void); // 成功时返回0,失败时返回SOCKET_ERROR
        
      • 调用WSACleanup函数后,Winsock库归还Windows系统,无法再调用Winsock相关函数,通常在程序结束前使用。

1、4 基于Windows的套接字相关函数

  • // 与linux socket函数提供相同功能。调用 socket 函数创建套接字
    #include <winsock2.h>
    SOCKET socket(int af, int type, int protocol);// 成功时返回套接字句柄,失败返回INVALID_SOCKET
    
  • // 调用 bind 函数分配ip地址和端口号
    #include <winsock2.h>
    int bind(SOCKET s, const struct sockaddr * name, int namelen);// 成功时返回0,失败返回SOCKET_ERROR
    
  • // listen函数将套接字转化为可接受连接状态,使其可接收客户端连接。
    #include <winsock2.h>
    int listen(SOCKET s, int backlog);// 成功返回0,失败返回SOCKET_ERROR
    
  • //accept 函数受理连接请求,如果在没有连接请求的情况下调用该函数,则不会返回,直到有连接请求为止
    #include <winsock2.h>
    SOCKET accept(SOCKET s, struct sockaddr * addr, int *addrlen); // 成功返回套接字句柄,失败返回INVLID_SOCKET
    
  • // 调用 connect 函数向服务器发送连接请求
    #include <winsock2.h>
    int connect(SOCKET s, const struct sockaddr *name,int namelen);//成功返回0,失败返回SOCKET_ERROR
    
  • // 关闭套接字
    #include <winsock2.h>
    int closesocket(SOCKET s); // 成功返回0,失败返回 SOCKET_ERROR
    
  • Windows文件句柄与套接字句柄

    • Windows句柄相当于Linux中的文件描述符,SOCKET类型是由typedef定义的保存套接字句柄整形值的数据类型。
  • Windows的服务端示例:

    • #include <stdio.h>
      #include <stdlib.h>
      #include <WinSock2.h>
      void ErrorHanding(const char* message);
      
      int main(int argc, char* argv[]) {
      	WSADATA wsaData;
      	SOCKET hServSock, hClntSock;
      	SOCKADDR_IN servAddr, clntAddr;
      
      	int szClntAddr;
      	char message[] = "Hello World!";
      	if (argc != 2) {
      		printf("Usage: %s <port>\n", argv[0]);
      		exit(1);
      	}
      
      	// 成功时返回0,失败时返回非0的错误值代码,设置程序中用到的Winsock版本
      	// 初始化套接字库
      	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
      		ErrorHanding("WSAStartup() error");
      	// 调用socket函数生成 服务器socket套接字, 成功时返回套接字句柄,失败返回INVALID_SOCKET
      	// 创建套接字
      	hServSock = socket(PF_INET, SOCK_STREAM, 0);
      	if (hServSock == INVALID_SOCKET)
      		ErrorHanding("socket() error");
      	
      	memset(&servAddr, 0, sizeof(servAddr));
      	servAddr.sin_family = AF_INET;
      	servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
      	servAddr.sin_port = htons(atoi(argv[1]));
      
      	// 调用 bind 函数分配ip地址和端口号
      	if (bind(hServSock, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
      		ErrorHanding("bind() error");
      	
      	// listen函数将套接字转化为可接受连接状态,使其可接收客户端连接。
      	// 称为服务器端套接字
      	if (listen(hServSock, 5) == SOCKET_ERROR)
      		ErrorHanding("Listen() error");
      
      	szClntAddr = sizeof(clntAddr);
      	// accept函数受理连接请求,如果在没有连接请求的情况下调用该函数,则不会返回,直到有连接请求为止
      	// accept 函数受理客户端请求
      	hClntSock = accept(hServSock, (SOCKADDR*)&clntAddr, &szClntAddr);
      	if (hClntSock == INVALID_SOCKET)
      		ErrorHanding("accept() error");
      
      	// send()函数向客户端传输数据
      	send(hClntSock, message, sizeof(message), 0);
      	closesocket(hClntSock);
      	closesocket(hServSock);
      	// 程序终止前注销套接字库
      	WSACleanup();
      	return 0;
      
      }
      
      void ErrorHanding(const char* message) {
      	fputs(message, stderr);
      	fputc('\n', stderr);
      	exit(1);
      }
      
  • 基于Windows端的客户端示例:

  • #include <stdio.h>
    #include <stdlib.h>
    #include <WinSock2.h>
    #pragma comment (lib, "ws2_32.lib")  //加载 ws2_32.dll
    void ErrorHanding(const char* message);
    
    
    int main(int argc, char* argv[]) {
    	WSADATA wsaData;
    	SOCKET hSocket;
    	SOCKADDR_IN servAddr;
    
    	char message[30];
    	int strlen;
    	if (argc != 3) {
    		printf("Usage: %s <IP> <port>\n", argv[0]);
    		exit(1);
    	}
    	// 初始化Winsock库
    	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
    		ErrorHanding("WSAStartup() error!");
    	// 创建套接字
    	hSocket = socket(PF_INET, SOCK_STREAM, 0);
    	if (hSocket == INVALID_SOCKET)
    		ErrorHanding("socket() error");
    
    	memset(&servAddr, 0, sizeof(servAddr));
    	servAddr.sin_family = AF_INET;
    	servAddr.sin_addr.s_addr = inet_addr(argv[1]);
    	servAddr.sin_port = htons(atoi(argv[2]));
    	
    	// 向服务端发送连接请求
    	if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
    		ErrorHanding("connect() error");
    
    	// recv函数接收服务器端发送来的数据
    	strlen = recv(hSocket, message, sizeof(message) - 1, 0);
    	if (strlen == -1)
    		ErrorHanding("read() error");
    	printf("Message from serve : %s \n", message);
    
    	closesocket(hSocket);
    	WSACleanup();
    	return 0;
    
    }
    
    void ErrorHanding(const char* message) {
    	fputs("message", stderr);
    	fputc('\n', stderr);
    	exit(1);
    }
    
    • 编译运行hello_server_win.cpp

      • gcc hello_server_win.cpp -o hServerWin -lws2_32 // 需要添加-lws2_32
        
      • 需要添加-lws2_32,否则会报 “Undfined Reference to WSAStartup @8”等错误,解释在这里。

    • 编译运行hello_client_win.cpp

      • gcc hello_client_win.cpp -o hClientWIn -lws2_32
        
    • 运行结果:

      • hServerWin 9190
        
        在这里插入图片描述
        在这里插入图片描述
  • 基于Windows的I/O函数

  • Windows严格区分文件I/O函数和套接字I/O函数。

  • Winsock数据传输函数

      #include <winsock2.h>
      int send(SOCKET s, const char *buf, int len, int flags);// 成功时返回传输字节数,失败返回SOCKET_ERROR
    
    • s : 数据传输对象连接的套接字句柄
    • buf : 保存待传输数据的缓存地址值
    • len : 要传输的字节数
    • flags:传输数据时用到的多种选项信息
      #include <winsock2.h>
      int recv(SOCKET s, const char * buf , int len, int flags); // 成功返回接收的字节数,失败返回SOCKET_ERROR
    
    • s : 数据传输对象连接的套接字句柄
    • buf : 保存待传输数据的缓存地址值
    • len : 要传输的字节数
    • flags:传输数据时用到的多种选项信息
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值