欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

TCP/IP网络编程学习笔记

程序员文章站 2022-06-30 18:20:16
...

最近学习网络编程,做了一些笔记,简要、明确,方便自己查看,也希望能让更多人看到,网络编程的大概过程就是这样。

1、网络编程中接受(server)连接请求的套接字创建过程

第一步:调用socket函数创建套接字

第二步:调用bind函数分配IP地址和端口号

第三步:调用listen函数转为可接收请求状态

第四步:调用accept函数受理连接请求

2、服务器端创建的套接字又称为服务器端套接字或监听套接字

3、客户端只有“调用socket函数创建套接字”和“调用connect函数向服务器端发送连接请求”两个步骤

4、文件描述符

每当生成文件或套接字,操作系统将返回分配给它们的整数。这个整数就将成为程序员和操作系统之间良好沟通的渠道。实际上,文件描述符只不过是为了方便称呼操作系统创建的文件或者套接字而赋予的数而已。

文件描述符有时也称为文件句柄,但“句柄”主要是Windows中的术语。

5、协议

简言之,协议就是为了完成数据交换而约定好的约定。

6、创建套接字

#include <sys/socket.h>

int socket(int domain, int type, int protocol)
    -> 成功时返回文件描述符,失败时返回-1
  • domain 套接字中使用的协议簇(Protocol Family)信息
  • type 套接字数据传输类型信息
  • protocol 计算机间通信中使用的协议信息

协议簇

名称 协 议 簇
PF_INET IPv4互联网协议簇
PF_INET6 IPv6互联网协议簇
PF_LOCAL 本地通信的UNIX协议簇
PF_PACKET 底层套接字的协议簇
PF_IPX IPX Novell协议簇

套接字中实际采用的最终协议信息是通过socket函数的第三个参数传递的。在指定的协议簇范围内通过第一个参数决定第三个参数。

套接字类型(type)

套接字类型指的是套接字的数据传输方式,通过socket函数的第二个参数传递,只有这样才能决定创建的套接字的数据传输方式。

套接字类型1:面向连接的套接字(SOCKET_STREAM)

这种方式的特征:

  • 传输过程中数据不会消失
  • 按序传输数据
  • 传输的数据不存在数据边界

一句话概括面向连接的套接字:“可靠的、按序传递的、基于字节的面向连接的数据传输方式的套接字”

套接字类型2:面向消息的套接字(SOCK_DGRAM)

这种方式的特征:

  • 强调快速传输而非传输顺序
  • 传输的数据可能丢失也可能损毁
  • 传输的数据有数据边界
  • 限制每次传输的数据大小

面向消息的套接字比面向连接的套接字具有更快的传输速度,但无法避免数据丢失或损毁。

“不可靠的、不按序传递的、以数据的高速传输为目的的套接字”

协议的最终选择(第三个参数)

已经通过socket函数的前两个参数传递了协议簇信息和套接字数据传输方式,一般情况下,这两个参数已经可以创建所需套接字。所以大部分情况下可以向第三个参数传递0,除非遇到以下这种情况:

“同一协议簇中存在多个数据传输方式相同的协议”

创建如下套接字:“IPv4协议簇中面向连接的套接字”,参数PF_INET指IPv4网络协议簇,SOCK_STREAM是面向连接的数据传输。满足这两个条件的协议只有IPPROTO_TCP,因此:

int tcp_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);

下面创建如下套接字:“IPv4协议簇中面向消息的套接字”

int udp_socket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);

7、地址族与数据序列

IP是为了收发网络数据而分配给计算机的值。端口号并非赋予计算机的值,而是为了区分程序中创建的套接字而分配给套接字的序号。

网络地址(Internet Address)

IP地址分为两类:IPv4和IPv6

总结:“IP是用于定位到特定的主机,端口号是用于定位到某个主机上特定的程序。”

端口号不允许重复,但是TCP套接字和UDP套接字不会公用端口号,所以允许重复。例如某TCP套接字使用9190号端口,则其他的TCP套接字不能在使用9190,但是UDP套接字可以使用。

总之,数据传输目标地址同时包含IP地址和端口号,只有这样,数据才会被传输到最终的应用程序。

地址信息的表示

应用程序中使用的IP地址和端口号以结构体的形式给出了定义

struct sockaddr_in
{
    sa_family_t     sin_family;     //地址族(Address Family)
    uint16_t        sin_port;       //16位TCP/UDP端口号
    struct in_addr  sin_addr;       //32位IP地址
    char            sin_zero[8];    //不使用
}

该结构体中提到的另一个结构体in_addr定义如下,它用来存放32位IP地址

struct in_addr
{
    In_addr_t       s_addr;         //32位ipv4地址
}

网络字节序跟大端序相同

服务器端常见套接字初始化过程

int serv_sock;
struct sockaddr_in serv_addr;
char *serv_port = "9190";

//创建服务器端套接字(监听套接字)
serv_sock=socket(PF_INET,SOCK_STREAM,0);

//地址信息初始化
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(serv_addr));

//分配地址信息
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

8、基于TCP的服务器端和客户端

根据数据传输方式的不同,基于网络协议的套接字一般分为TCP套接字和UDP套接字。因为TCP套接字是面向连接的,因此又称为基于流(stream)的套接字。

服务器端处于等待连接请求状态是指:客户端请求连接时,受理连接前一直使请求连接处于等待状态。

hello world服务器端代码,hello_server.c

int main(int argc, char *argv[]){
    int serv_sock;
    int clnt_sock;

    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", agrv[0]);
        exit(1);
    }

    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    if(serv_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=htonl(INADDR_ANY);
    serv_addr.sin_port=htons(atoi(argv[1]));

    if(bind(serv_sock, (struct sockaddr*)serv_addr, sizeof(serv_addr))==-1)
        error_handling("bind() error");

    if(listen(serv_sock, 5)==-1)
        error_handling("listen() error");

    clnt_addr_size = sizeof(clnt_addr);
    clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr,&clnt_addr_size);
    if(clnt_sock == -1){
        error_handling("accept() error");
    }

    write(clnt_sock, message, sizeof(message));
    close(clnt_sock);
    close(serv_sock);
    return 0;
}

void error_handling(char *message){
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

Hello World客户端,hello_world.c

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(0);
    }

    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]));

    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);
}

9、基于UDP的服务器端/客户端

UDP套接字的特点

通过信件说明UDP的工作原理,它与UDP特性完全相符。寄信前应先在信封上填好寄信人和收信人的地址,之后贴上邮票放进邮筒即可。当然,信件的特点使我们无法确认对方是否收到。另外,邮寄过程中也可能发生信件丢失的情况。也就是说,信件是一种不可靠的传输方式。与之类似,UDP提供的同样是不可靠的数据传输服务。

流控制是区分UDP和TCP的最重要的标志。TCP的生命在于流控制。TCP在不可靠的IP层进行流控制,而UDP就没有这种流控制机制。

UDP最重要的作用就是根据端口号将传到主机的数据包交付给最终的UDP套接字。

TCP比UDP慢的原因通常有以下两点:

  • 收发数据前后进行的连接设置及清除过程
  • 收发数据过程中为保证可靠性而添加的流控制

实现基于UDP的服务器/客户端

UDP中服务器端和客户端没有连接

UDP服务器端/客户端不像TCP那样在连接状态下交换数据,因此与TCP不同,无需经过连接过程。也就是说,不必调用TCP连接过程中调用的listen函数和accept函数。UDP中只有创建套接字和数据交换过程。

UDP服务器端和客户端均只需1个套接字

TCP中,套接字之间应该是一对一的关系。若要向10个客户端提供服务,则除了守门的服务器套接字外,还需要10个服务器端套接字。但在UDP中,不管是服务器端还是客户端都只需要1个套接字。

基于UDP的数据I/O函数

创建好TCP套接字后,传输数据时无需再添加地址信息。因为TCP套接字将保持与对方套接字的连接。换言之,TCP套接字知道目标地址信息。但UDP套接字不会保持连接状态(UDP套接字只有简单的邮筒功能),因此每次传输数据都要添加目标地址信息。这相当于寄信前在信件中添写地址。

传输数据时调用的UDP相关函数

#include<sys/socket.h>

ssize_t sendto(int sock, void *buff, size_t nbytes, int flags, struct sockaddr *to, socklen_t addrlen);

    ->成功时返回传输的字节数,失败时返回-1

    sock    用于传输数据的UDP套接字文件描述符
    buff    保存待传输数据的缓冲地址值
    nbytes  待传输的数据长度,以字节为单位
    flags   可选项参数,若没有则传递0
    to      存有目标地址信息的sockaddr结构体变量的地址值
    addrlen 传递给参数to的地址值结构体变量长度

接收UDP数据的函数

#include<sys/socket.h>

ssize_t recvfrom(int sock, void *buff, size_t nbytes, int flags, struct sockaddr *from, socklen_t *addrlen);

    ->成功时返回接收的字节数,失败时返回-1

编写UDP程序时最核心的部分就是上述两个函数。

基于UDP的回声服务器端/客户端

UDP和TCP不同,不存在请求连接和受理过程,因此在某种意义上无法明确区分服务器端和客户端,只是因其提供服务而称为服务器端。

uecho_server.c

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

#define BUF_SIZE 30
void error_handing(char *message);

int main(int argc, char *argv){
    int serv_sock;
    char message[BUF_SIZE];
    int str_len;
    socklen_t clnt_adr_sz;
    struct sockaddr_in serv_adr,clnt_adr;
    if(argc != 2){
        printf("Usage: %s <port>\n",argv[0]);
        exit(1);
    }

    serv_sock = socket(PF_INET,SOCK_DGRAM, 0);
    if(serv_sock == -1)
        error_handling("UDP socket creation error");

    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family=AF_INET;
    serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
    serv_adr.sin_port=htons(atoi(argv[1]));

    if(bind(serv_sock,, (struct sockaddr*)&serv)_adr, sizeof(serv_addr))==-1)
        error_handling("bind() error");

    while(1){
        clnt_adr_sz=sizeof(clnt_adr);
        str_len=recvfrom(serv_sock, message, BUF_SIZE, 0, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
        sendto(serv_sock, message, str_len, 0, (struct sockaddr*)&clnt_adr, clnt_adr_sz);
    }
    close(serv_sock);
    return 0;
}

void error_handling(char *message){
    fputs(message, stderr);
    fputc('\n',stderr);
    exit(1);
}

uecho_client.c 不存在connect函数调用

#include <"与uecho_server.c相同">

#define BUF_SIZE 30
void error_handling(char *message);

int main(int argc, char *argv[]){
    int sock;
    char message[BUF_SIZE];
    int str_len;
    socklen_t adr_sz;

    struct sockaddr_in serv_adr, clnt_adr;
    if(argc != 3){
        printf("Usage: %s <IP> <port>\n", argv[0]);
        exit(1);
    }

    sock = socket(PF_INET, SOCK_DGRAM, 0);
    if(sock ==-1){
        error_handling("socket() error");
    }

    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family=AF_INET;
    serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
    serv_addr.sin_port=atoi(argv[2]));

    while(1){
        fputs("Inert message(q to quit): ",stdout);
        fgets(message, sizeof(message), stdin);
        if(!strcmp(message,"q\n") || !strcmp(message, "Q\n"))
            break;

        sendto(sock, message, strlen(message), 0, (struct sockadr*)&serv_adr, sizeof(serv_adr));
        adr_sz=sizeof(from_adr);
        str_len=recvfrom(sock, message, BUF_SIZE, 0, (struct sockaddr*)&from_adr, &adr_sz);
        message[str_len]=0;
        printf("Message from server: %s", message);
    }
    close(sock);
    return 0
}

void error_handling(char *message){
    fputs(message, stderr);
    fputc('\n',stderr);
    exit(1);
}

TCP是在调用connect函数时自动分配IP地址和端口号的,而UDP呢,是在调用sendto函数时自动分配IP和端口号的。

相关标签: 网络编程 socket