自学内容网 自学内容网

TCP IP网络编程

TCP IP网络编程

一、基础知识(TCP)

1)Linux

1. socket()
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
/*
domain: 套接字中使用的协议族信息
type : 套接字数据传输类型信息
protocol :计算机间通信中使用的协议信息。【最终决定采用什么协议】
*/
int socket(int domain, int type, int protocol);

domain

名称描述
PF_INETIPv4互联网协议族
PF_INET6IPv6互联网协议族
PF_LOCAL本地通信的UNIX协议族
PF_PACKET底层套接字的协议族
PF_IPXIPX Novell协议族

type

  1. 面向连接的套接字(SOCK_STREAM) 【跟TCP一样】

特点:可靠的,按序传递的,基于字节的面向连接的数据传输方式的套接字。

  1. 面向消息的套接字(SOCK_DGRAM) 【跟UDP一样】

特点:不可靠的,不按序传递的,以数据的高速传输为目的的套接字。

protocol

传递前两个参数即可创建所需套接字,第三个参数是为了以下情况:

同一协议族中存在多个数据传输方式相同的的协议。这时需要通过第三个参数具体指定协议信息。

IPv4协议族中面向连接的套接字,协议只有IPPROTO_TCP。

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

这个套接字称为TCP套接字。

IPv4协议族中面向消息的套接字,协议只有IPPROTO_UDP

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

这个套接字称为UDP套接字。

2.bind()
2.1前提
struct sockaddr_in
{
    sa_family_tsin_family; //地址族
    uint16_tsin_port;   //16位TCP/UDP端口号
    struct in_addrsin_addr;//32位IP地址
    charsin_zero[8];//不使用
}

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

sin_family

地址族含义
AF_INETIPv4网络协议中使用的地址族
AF_INET6IPv6网络协议中使用的地址族
AF_LOCAL本地通信中采用的UNIX协议的地址族

sin_port

它以网络字节序保存。

sin_addr

以网络字节序保存。

sin_zero

无特殊含义。只是为了使结构体sockaddr_in的大小与sockaddr结构体保持一致而插入的成员。必须填充为0,否则无法得到想要的结果。

 struct sockaddr 
 {
      sa_family_t  sa_family; //地址族
       char        sa_data[14]; //地址信息
}

此结构体成员sa_data保存的地址信息中需包含IP地址和端口号,剩余部分应填充0,这也是bind函数要求的。而这对于包含地址信息来讲非常麻烦,继而就有了新的结构体sockaddr_in。若按照之前的讲解填写sockaddr_in结构体,则将生成符合bind函数要求的字节流。最后转换为sockaddr型的结构体变量,再传递给bind函数即可。

2.2字节序与网络字节序

CPU向内存保存数据的方式有2种

  1. 大端序:高位字节存放到低地址。
  2. 小端序:高位字节存放到高位地址。

因为这种情况,所以网络传输时规定了统一的字节序。大端序,这就叫做网络字节序。

传输数据时,先把数据数组转化成大端序格式再进行网络传输。

2.3 字节序转换
unsigned short htons(unsigned short);
unsigned short ntohs(unsigned short);
unsigned long htonl(unsigned long);
unsigned long ntohl(unsigned long);

解释:

  • htons中的h代表主机(host)字节序。
  • htons中的n代表网络(network)字节序。
  • s指的是short
  • l值的是long

htons的含义:把short型数据从主机字节序转化为网络字节序。

ntohs的含义:把short型数据从网络字节序转化成主机字节序。

通常,以s作为后缀的函数中,s代表2个字节shont,因此用于端口号转换;以1作为后缀的函数中,1代表4个字节,因此用于IP地址转换。

注意:除了向sockaddr in结构体变量填充数据外,其他情况无需考虑字节序问题。

2.4 字符串信息转化成网络字节序的整数型
#include <arpa/inet.h>

int_addr_t inet_addr(const char* string);
/* 把点分十进制ip地址,转化为32位整型数值,并按照网络字节序*/

成功返回32为整型数值,失败返回INADDR_NONE

同样功能的函数还有:

#include <arpa/inet.h>

int inet_aton(const char* string, struct in_addr* addr);

实际编程中若要调用inet_addr函数,需将转换后的IP地址信息代入sockaddr_in结构体中声明的in_addr结构体变量。而inet_aton函数则不需此过程。原因在于,若传递in_addr结构体变量地址值,函数会自动把结果填人该结构体变量。

char* addr = "127.232.124.79";
struct sockaddr_in addr_inet;
inet_acton(addr,&addr_inet.sin_addr);

将32为数值转化为字符串

#include <arpa/inet.h>
char* inet_ntoa(struct in_addr adr);

注意!!!

该函数将通过参数传入的整数型IP地址转换为字符串格式并返回。但调用时需小心,返回值类型为char指针。返回字符串地址意味着字符串已保存到内存空间,但该函数未向程序员要求分配内存,而是在内部申请了内存并保存了字符串。也就是说,调用完该函数后,应立即将字符串信息复制到其他内存空间。因为,若再次调用inet_ntoa函数,则有可能覆盖之前保存的字符串信息。总之,再次调用inet_ntoa函数前返回的字符串地址值是有效的。若需要长期保存,则应将字符串复制到其他内存空间。

2.5 INADDR_ANY

每次创建服务器端套接字都要输入IP地址会有些繁琐,所以采用常数INADDR_ANY分配服务器端的IP地址,可自动获取运行服务器端的计算机IP地址,不必亲自输入。

addr.sin_addr.s_addr = htonl(INADDR_ANY);

2.6 bind()

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
/*
*/
int bind(int sockfd, const struct sockaddr *addr,
          socklen_t addrlen);

成功返回0,失败返回-1

3.listen()
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
/*
sockfd : 希望进入等待连接请求状态的套接字文件描述符
backlog:连接请求等待队列的长度。
*/
int listen(int sockfd, int backlog);
4.accept()
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
/*
sock:服务器套接字的文件描述符
addr:保存发起连接请求的客户端地址信息的变量地址值,调用函数后向传递来的地址变量参数填充客户端地址信息。
addrlen:第二个参数addr结构体的长度,但是存有长度的变量地址。函数调用完成后,该变量即被填入客户端地址长度。
*/
int accept(int sockfd, struct sockaddr *addr, 
           socklen_t *addrlen);
5.connect()
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

/*
sockfd:客户端套接字文件描述符
addr:保存目标服务器端地址信息的变量地址值
addrlen:以字节为单位传递地址变量长度
*/
int connect(int sockfd, const struct sockaddr *addr,
            socklen_t addrlen);

注意

客户端的IP地址和端口在调用connect函数时自动分配,无需调用bind函数进行分配。IP用计算机的IP,端口随机。

6.案例小结
6.1服务器端
#include <cstdio>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cstring>
#include <arpa/inet.h>

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

int main(int argc, char** argv)
{
    if(argc != 2)
    {
        printf("Usage : %s <port> \n",argv[1]);
        exit(1);
    }

    // 创建套接字
    int serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    if(serv_sock < 0)
    {
        error_handling("serv_sock() error");
    }

    struct sockaddr_in serv_addr;
    // 初始化地址
    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]));

    // 分配地址
    int ret = bind(serv_sock,(struct sockaddr*)&serv_addr, sizeof(serv_addr));

    if(ret < 0)
    {
        error_handling("bind() error");
    }

    // 开始监听
    int ret1 = listen(serv_sock, 5);
    if(ret1 < 0)
    {
        error_handling("listen() error");
    }

    int client_sock;
    struct sockaddr_in client_addr;

    socklen_t client_addr_size = sizeof(client_addr);

    // 接受访问
    client_sock = accept(serv_sock, (struct sockaddr*) &client_addr, &client_addr_size);
    if(client_sock < 0)
    {
        error_handling("accept() error");
    }

    char message[] = "hello,world!";

    // 传输数据
    write(client_sock, message, sizeof(message));

    // 传输完成,关闭套接字
    close(client_sock);
    close(serv_sock);

    return 0;
}


6.2 客户端
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>

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

int main(int argc, char** argv)
{
    if(argc != 3)
    {
        printf("Usage : %s <ip> <port> \n", argv[0]);
        exit(1);
    }
    int sock(-1);

    // 创建套接字
    sock = socket(PF_INET, SOCK_STREAM, 0);
    if(sock < 0)
    {
        perror("sock()");
        // error_handling("socket() error");
    }

    // 初始化地址和端口号
    struct sockaddr_in serv_addr;

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

    // 发送请求
    int ret = connect(sock, (struct sockaddr*)&serv_addr,sizeof(serv_addr));
    if(ret < 0)
    {
        perror("connect()");
        // error_handling("connect() error");
        exit(1);
    }

    char message[30];
    // 接收数据
    int str_len = read(sock,message,sizeof(message)-1);
    if(str_len < 0)
    {
        perror("read()");
        // error_handling("read() error");
    }
    printf("Message from server : %s \n", message);

    // 关闭套接字
    close(sock);

    return 0;
}

7.半关闭

Linux的close函数意味着完全断开连接。完全断开不仅指无法传输数据,而且也不能接收数据。

开一部分连接是指,可以传输数据但无法接收,或可以接收数据但无法传输。顾名思义就是只关闭流的一半。

#include <sys/socket.h>

int shutdown(int sockfd, int how);

how

名称含义
SHUT_RD断开输入流
SHUT_WR断开输出流
SHUT_RDWR同时断开I/O流

2)Window

1. socket()
#include <winsock2.h>
SOCKET socket(int af, int type, int protocol);

这些参数与Linux一样,只是返回值不同。SOCKET其实就是整数类型,微软把他重定义了。

出现错误时,返回INVALID_SOCKET。

#include <winsock2.h>
SOCKET soc = socket(PF_INET, SOCK_STREAM,IPPROTO_TCP);
if(soc == INVALID_SOCKET)
    errorHandling("........");
2.bind()

与Linux完全相同。但是,Windows中不存在inet aton函数

3.listen()

同Linux

4.accept()

同Linux

5.recv()

同linux的read()

6.send()

同Linux的write()

二、UDP基础知识

1)Linux

1.sendto()
#include <sys/types.h>
#include <sys/socket.h>
/*

sockfd : 用于传输数据的UDP套接字文件描述符
buff : 保存待传输数据的换成地址值
nbytes :待传输的数据长度,以字节为单位
flags :可选参数,若没有传递0
to : 存有目标地址信息的sockaddr结构体变量的地址值
addrlen :传递给参数to的地址值结构体变量长度
*/
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);

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

调用该函数时自动分配IP和端口号

2.recvfrom()
#include <sys/types.h>
#include <sys/socket.h>
/*
sockfd : 用于接收数据的UDP套接字文件描述符
buff : 保存接收数据的缓冲地址值
nbytes : 可接收的最大字节数,故无法超过参数buff所指的缓冲大小
flags : 可选参数,若没有则传入0
src_addr : 存有发送端地址信息的地址值
addrlen :保存参数from的结构体变量长度的变量地址值
*/
ssize_t recvfrom(int sockfd, void *buf, size_t len, 
                 int flags,
  struct sockaddr *src_addr,
                 socklen_t *addrlen);
3.已连接UDP套接字

如果对于某个目标,需要一直发送消息,那么就可以使用已连接的UDP。

和TCP套接字一样,使用connect函数,使用这个函数之后,就可以使用write和read函数

4.案例

服务器端

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>

const int BUF_SIZE = 30;

int main(int argc, char** argv)
{
    // 判断
    if(argc != 2)
    {
        fprintf(stderr,"Usage : %s <port> \n",argv[0]);
        exit(1);
    }
    int serv_sock(-1);
    // 创建套接字
    serv_sock = socket(PF_INET,SOCK_DGRAM,0);
    if(serv_sock < 0)
    {
        perror("socket()");
        exit(1);
    }
    // 初始化IP地址和端口号
    struct sockaddr_in sock_addr;
    memset(&sock_addr, 0, sizeof(sock_addr));
    sock_addr.sin_family = AF_INET; //地址族
    sock_addr.sin_addr.s_addr = htonl(INADDR_ANY);//ip地址自动获取本机ip
    sock_addr.sin_port = htons(atoi(argv[1]));

    // 分配ip和端口号
    int ret = bind(serv_sock, (struct sockaddr*)&sock_addr, sizeof(sock_addr));
    if(ret < 0)
    {
        perror("bind()");
        exit(1);
    }

    // 初始化客户请求
    int client_sock(-1);
    struct sockaddr_in client_addr;
    socklen_t client_addr_len = sizeof(client_addr);

    // 保存客户端发来的信息
    char message[BUF_SIZE];
    int str_len(0);

    while(true)
    {
        // 接收数据
        str_len = recvfrom(serv_sock, message, BUF_SIZE, 0,
                          (struct sockaddr*)&client_addr, 
                          &client_addr_len);

        // 发送数据
        sendto(serv_sock, message, str_len, 0,
        (struct sockaddr*)&client_addr, client_addr_len);
        

    }
    close(serv_sock);
    return 0;

}

客户端

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

const int BUF_SIZE = 30;

int main(int argc, char** argv)
{
    if(argc != 3)
    {
        printf("Usage : %s <ip> <port> \n", argv[0]);
        exit(1);
    }

    // 创建套接字
    int clt_sock(-1);
    clt_sock = socket(PF_INET, SOCK_DGRAM, 0);
    if(clt_sock < 0)
    {
        perror("socket()");
        exit(1);
    }
    // 初始化服务器地址
    struct sockaddr_in serv_addr;
    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]));

    char message[BUF_SIZE];

    struct sockaddr_in from_addr;

    memset(&from_addr, 0, sizeof(from_addr));
    socklen_t from_addr_len = sizeof(from_addr);

    while (true)
    {
        fputs("Input message(Q to quit): ",stdout);
        fgets(message, BUF_SIZE, stdin);

        if(!strcmp(message,"q\n") || !strcmp(message, "Q\n"))
            break;
        
        // write(clt_sock, message, strlen(message));
        // 发送数据
        sendto(clt_sock, message, strlen(message), 0,
              (struct sockaddr*)&serv_addr, sizeof(serv_addr));

        // int str_len = read(clt_sock, message, BUF_SIZE -1);
        // message[str_len] = 0;

        // 接收数据
        int len = recvfrom(clt_sock, message, BUF_SIZE, 0,
                          (struct sockaddr*)&from_addr, &from_addr_len);
        
        message[len] = 0;
        printf("Message from server : %s", message);

    }
    
    close(clt_sock);
    return 0;
}

2)Window

同Linux一模一样。

三、域名

1.由域名得到ip

#include <netdb.h>
extern int h_errno;

/* 通过域名获取ip地址*/
struct hostent *gethostbyname(const char *name);

struct hostent 
{
    char  *h_name;            /* official name of host */
    char **h_aliases;         /* alias list */
    int    h_addrtype;        /* host address type */
    int    h_length;          /* length of address */
    char **h_addr_list;       /* list of addresses */
}

成功返回结构体类型,失败返回NULL

  • h_name :该变量中存有官方域名
  • h_aliases :可以通过多个域名访问同一个主页。
  • h_addrtype :保存地址族信息
  • h_length : 保存IP地址长度
  • h_addr_list : 保存域名1对应的IP地址

2.通过IP地址获取域名

#include <sys/socket.h>       /* for AF_INET */
#include <netdb.h>

struct hostent *gethostbyaddr(const void *addr,
                              socklen_t len, 
                              int type);
  • addr : 含有IP地址信息的in_addr结构体的指针。
  • len :向第一个参数传递的地址信息的字节数,IPv4时为4,IPv6时为16
  • type:传递地址族信息,IPv4时为AF_INET,IPv6时为IP_INET6

3.Window

同Linux一样,连函数名和参数都相同。

四、套接字的多种选项

1.获取和设置套接字选项

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int getsockopt(int sockfd, int level, int optname,
                void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname,
                const void *optval, socklen_t optlen);
  • sockfd :用于查看选项套接字文件描述符
  • Level :要查看的可选项的协议层
  • optname :要查看的可选项名
  • optval :保存查看结果的缓冲地址值
  • optlen :向第四个参数optval传递的缓冲大小。调用函数后,该变量中保存通过第四个参数返回的可选项信息的字节数。

2.各种选项

直接man函数,查看选项。或者上网搜索。

3.Nagle算法

为防止因数据包多过而发生网络过载。

在这里插入图片描述

五、多进程服务端

1.案例

在这里插入图片描述

服务器端

/**
 * 多进程并发服务器
 */
#include <cstdio>
#include <cstdlib>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <cstring>

const int BUF_SIZE = 30;
void read_childproc(int sig)//声明信号处理函数
{
    pid_t pid= waitpid(-1, NULL, WNOHANG);//给子进程收尸
    printf("remove proc id : %d\n", pid); 
}

int main(int argc, char** argv)
{
    if(argc != 2)
    {
        fprintf(stderr,"Usage : %s <port>\n",argv[0]);
        return 1;
    }

    // 设置子进程结束信号
   struct sigaction act;
   act.sa_handler = read_childproc;//设置信号处理函数
   sigemptyset(&act.sa_mask);//置0
   act.sa_flags = 0;
   sigaction(SIGCHLD, &act, NULL);//设置信号



    // 创建socket
    int serv_sock(-1);
    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    if(serv_sock < 0)
    {
        perror("socket()");
        return 1;
    }
    // 初始化ip,port
    struct sockaddr_in serv_add;
    memset(&serv_add, 0, sizeof(serv_add));
    serv_add.sin_family = AF_INET;//IPv4协议族
    serv_add.sin_addr.s_addr = htonl(INADDR_ANY);//自动获取本机ip
    serv_add.sin_port = htons(atoi(argv[1]));

    //分配ip,port
    int ret = bind(serv_sock, (struct sockaddr*)&serv_add, sizeof(serv_add));
    if(ret < 0)
    {
        perror("bind()");
        return 1;
    }
    //进入请求等待 
    int ret0 = listen(serv_sock,5);
    if(ret0 < 0)
    {
        perror("listen()");
        return 1;
    }

    int clnt_sock(-1);//用来保存客户端套接字
    struct sockaddr_in clnt_addr;//用来保存客户端地址
    socklen_t clnt_size = sizeof(clnt_addr);

    while(true)
    {
        //接收客户端请求
        clnt_sock = accept(serv_sock,(struct sockaddr*)&clnt_addr, &clnt_size);
        if(clnt_sock < 0)
        {
            continue;
        }
        else
            puts("new client connection....");
        pid_t pid;
        pid = fork();//创建子进程
        if(pid < 0)//error
        {
            close(clnt_sock);
            continue;
        }
        else if(pid ==0)//child
        {
            close(serv_sock);//
            int str_len(0);
            char message[BUF_SIZE];
            while((str_len = read(clnt_sock,message,BUF_SIZE)) != 0)
            {
                write(clnt_sock, message, str_len);
            }
            close(clnt_sock);
            puts("client disconnected....");
            return 0;
        }
        else //父进程
        {
            close(clnt_sock);//从队列中删除

        }

    }

    close(serv_sock);
    return 0;
}

客户端

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

const int BUF_SIZE = 1024;

int main(int argc, char** argv)
{
    if(argc != 3)
    {
        printf("Usage : %s <ip> <port> \n", argv[0]);
        exit(1);
    }

    // 创建套接字
    int clt_sock(-1);
    clt_sock = socket(PF_INET, SOCK_STREAM, 0);
    if(clt_sock < 0)
    {
        perror("socket()");
        exit(1);
    }
    // 初始化服务器地址
    struct sockaddr_in serv_addr;
    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]));

    // 发送请求
    int ret = connect(clt_sock,(struct sockaddr*)&serv_addr, sizeof(serv_addr));
    if(ret < 0)
    {
        perror("connect()");
        exit(1);
    }
    else
    {
        puts("Connected.......");
    }

    char message[BUF_SIZE];

    while (true)
    {
        fputs("Input message(Q to quit): ",stdout);
        fgets(message, BUF_SIZE, stdin);

        if(!strcmp(message,"q\n") || !strcmp(message, "Q\n"))
            break;
        
        write(clt_sock, message, strlen(message));
        int str_len = read(clt_sock, message, BUF_SIZE -1);
        message[str_len] = 0;

        printf("Message from server : %s", message);

    }
    
    close(clt_sock);
    return 0;
}

2.分割I/O

在这里插入图片描述

服务器端如1一样。

客户端

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

const int BUF_SIZE = 30;

void read_routine(int sock, char* buf)
{
    while(true)
    {
        int str_len = read(sock, buf, BUF_SIZE);
        if(str_len == 0)
            return ;
        buf[str_len] = 0;
        printf("<Message from server> : %s", buf);
    }

}

void write_routine(int sock, char* buf)
{
    while(1)
    {
        fgets(buf, BUF_SIZE, stdin);
        if(!strcmp(buf, "q\n") || !strcmp(buf,"Q\n"))
        {
            shutdown(sock, SHUT_WR);//关闭输出流,但是不关闭输入流
            return;
        }

        write(sock, buf, strlen(buf));
    }
}

int main(int argc, char** argv)
{
    if(argc != 3)
    {
        printf("Usage : %s <ip> <port> \n", argv[0]);
        exit(1);
    }

    // 创建套接字
    int clt_sock(-1);
    clt_sock = socket(PF_INET, SOCK_STREAM, 0);
    if(clt_sock < 0)
    {
        perror("socket()");
        exit(1);
    }
    // 初始化服务器地址
    struct sockaddr_in serv_addr;
    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]));

    // 发送请求
    int ret = connect(clt_sock,(struct sockaddr*)&serv_addr, sizeof(serv_addr));
    if(ret < 0)
    {
        perror("connect()");
        exit(1);
    }
    else
    {
        puts("Connected.......");
    }

    char message[BUF_SIZE];

    // 创建子进程
    pid_t pid;
    pid = fork();

    if(pid < 0)// error
    {
        perror("fork()");
        return 1;
    }
    else if (pid == 0) //child
    {
        write_routine(clt_sock, message);

    }
    else
    {
        read_routine(clt_sock, message);
    }

    close(clt_sock);
    return 0;
}

六、进程间通信

1.基于管道(PIPE)的通信

#include <unistd.h>

int pipe(int pipefd[2]);

成功返回0,失败返回-1.

  • pipefd[0] : 通过管道接收数据时使用的文件描述符,即管道出口。
  • pipefd[1] :通过管道传输数据时使用的文件描述,即管道入口。
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#include <string.h>
#include <wait.h>

const int BUF_SIZE = 40;

int main()
{
    int fd[2];
    
    int ret = pipe(fd);
    if(ret < 0)
    {
        perror("pipe()");
        return 1;
    }

    pid_t pid;
    char message[BUF_SIZE];
    pid = fork();
    if(pid < 0)
    {
        perror("fork()");
        return 1;
    }
    else if(pid == 0)
    {
    //    sleep(4);
       int str_len = read(fd[0],message,BUF_SIZE);
       message[str_len] = 0;

       printf("child read : %s\n", message);
       close(fd[0]);
       close(fd[1]);  
    }
    else
    {
        // printf("in :\n");
        // fputs(message, stdin);
         char msg[] = "who are you ?";
        write(fd[1], msg, strlen(msg));
        
        close(fd[0]);
        close(fd[1]);

        wait(NULL);
    }
    return 0;
}

七、并发服务器

1.I/O复用

1.1 select()

该函数,移植性比较好。

#include <sys/select.h>

int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, 
           struct timeval *timeout);

void FD_CLR(int fd, fd_set *set);
int  FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);

struct timeval
{
  long    tv_sec;         /* seconds */
  long    tv_usec;        /* microseconds */
};
1.2.epoll()

这个函数仅仅适用于Linux,

linux的方言,不可以移植。

#include <sys/epoll.h>
/*
创建一个epoll描述符。
size 可以随便填,给个正数即可
*/
int epoll_create(int size);
int epoll_create1(int flags);

epoll_ctl()可以向指定的 epoll 上下文中加入或删除文件描述符:

#include <sys/epoll.h>
/* 管理一个epoll描述符 */
int epoll_ctl(int epfd, int op, int fd, 
              struct epoll_event *event);

op 的选项

名称描述
EPOLL_CTL_ADD把fd指定的文件添加到epfd指定的epoll实例监听集中
EPOLL_CTL_DEL把fd指定的文件从epfd指定的epoll监听集中删掉
EPOLL_CTL_MOD使用event改变在已有fd上的监听行为
typedef union epoll_data
{
      void        *ptr;
      int          fd;
      uint32_t     u32;
      uint64_t     u64;
} epoll_data_t;

struct epoll_event 
{ 
   uint32_t     events;      /* Epoll events */
   epoll_data_t data;        /* User data variable */
};

events参数

名称描述
EPOLLERR文件出错。即使没有设置,这个事件也是被监听的
EPOLLET在监听文件上开启边沿触发,默认行为是水平触发
EPOLLHUP文件被挂起,即使没有设置,这个事件也是被监听的
EPOLLIN文件未阻塞,可读
EPOLLONESHOT在一次事件产生并处理后,文件不再被监听。(必须指定新事件)
EPOLLOUT文件未阻塞,可写
EPOLLPRI高优先级的带外数据可读
#include <sys/epoll.h>

/* wait for an I/O event on an epoll file descriptor */
int epoll_wait(int epfd, struct epoll_event *events,
               int maxevents, int timeout);

如果 timeout 为0,即使没有事件发生,调用也立即返回,此时调用返回0。如果 timeout 为 -1,调用将一直等待到有事件发生。
当调用返回,epoll_event 结构体中的 events 字段描述了发生的事件。 data字段包含了所有用户在调用 epoll_ctl()前的设置。

1.2.2 案例
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#include <cstring>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/epoll.h>


const int BUF_SIZE = 100;

int main(int argc, char** argv)
{
    if(argc != 2)
    {
        printf("Usage %s <port> \n",argv[0]);
        return 1;
    }
    int sock_serv;

    // 创建套接字
    sock_serv = socket(PF_INET, SOCK_STREAM, 0);
    if(sock_serv < 0)
    {
        perror("socket()");
        return 1;
    }
    // ip,port
    struct sockaddr_in serv_add;
    memset(&serv_add, 0, sizeof(serv_add));
    serv_add.sin_family = AF_INET;
    serv_add.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_add.sin_port = htons(atoi(argv[1]));

    int ret = bind(sock_serv,(struct sockaddr*)&serv_add, sizeof(serv_add));
    if(ret < 0)
    {
        perror("bind()");
        return 1;
    }

    int ret1 = listen(sock_serv, 5);
    if(ret1 < 0)
    {
        perror("listen()");
        return 1;
    }

    int sock_clnt;
    // struct timeval mytime;
    // fd_set readset;
    // FD_ZERO(&readset);
    // 把服务器套接字放入监视集合中
    // FD_SET(sock_serv,&readset);
    // int fd_max = sock_serv;
    // fd_set copy_readset;
    struct sockaddr_in clnt_addr;

    // 创建epoll
    int epfd = epoll_create(5);
    // 设置事件
    struct epoll_event* epevent;//保存返回的事件
    struct epoll_event event;//保存一开始的事件
    event.events = EPOLLIN;
    event.data.fd = sock_serv;

    // 给epoll添加描述符和事件
    epoll_ctl(epfd, EPOLL_CTL_ADD, sock_serv, &event);

    //初始化
    epevent =(struct epoll_event*) malloc(sizeof(epevent)*5);

    char message[BUF_SIZE];
    while(true)
    {
        // copy_readset = readset;
        // mytime.tv_sec = 5;
        // mytime.tv_usec = 5000;
        // int ans = select(fd_max + 1, &copy_readset, NULL, NULL, &mytime);
        int epoll_count = epoll_wait(epfd, epevent, 5, -1);
        if(epoll_count < 0)
        {
            perror("epoll_wait()");
            break;
        }
        else if(epoll_count == 0)
        {
            // printf("time over \n");
            continue;
        }
        else 
        {
            for(int i = 0; i < epoll_count; ++i)
            {
                // if(FD_ISSET(i, &copy_readset))
                if(epevent[i].data.fd == sock_serv)
                {
                    // if(i == sock_serv)//sock_serv有请求
                    // {
                        socklen_t clnt_size = sizeof(clnt_addr);
                        sock_clnt = accept(sock_serv, (struct sockaddr*)&sock_clnt,&clnt_size);
                        if(sock_clnt < 0)
                            {
                                continue;
                            }
        
                        // FD_SET(sock_clnt,&readset);
                        // if(fd_max < sock_clnt)
                        // {
                        //     fd_max = sock_clnt;
                        // }
                        struct epoll_event clnt_event;
                        clnt_event.data.fd = sock_clnt;
                        clnt_event.events = EPOLLIN;
                        epoll_ctl(epfd, EPOLL_CTL_ADD, sock_clnt, &clnt_event);

                        printf("Client %d is connecting...\n", sock_clnt);

                }
                else
                {
                        // int str_len = read(i, message, BUF_SIZE);
                    int str_len = read(epevent[i].data.fd, message, BUF_SIZE);
                    if(str_len == 0) //关闭请求
                    {
                            // FD_CLR(i, &copy_readset);
                         epoll_ctl(epfd, EPOLL_CTL_DEL, sock_clnt,NULL);
                        close(i);
                        printf("close client: %d \n", i);
                    }
                    else
                    {
                        write(epevent[i].data.fd, message, str_len);
                    }
                }
            }

        }
                 
    }
    close(epfd);
    close(sock_serv);   
    return 0;
}

1.2.3边沿触发与条件触发

条件触发方式中,只要输入缓冲有数据就会一直通知该事件。

边缘触发中输入缓冲收到数据时仅注册1次该事件。即使输入缓冲中还留有数据,也不会再注册。边缘触发方式下,以阻塞方式工作的read和write函数有可能引起服务器端的长时间停顿。因此,边缘触发方式中一定要采用非阻塞read和write函数。

将套接字改为非阻塞方式的方法

#include <unistd.h>
#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* arg */ );
名称含义
F_GETFL获得fd的文件描述符属性
F_SETFL更改文件描述符属性

将文件改为非阻塞模式

int flag = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flag | O_NONBLOCK);

2.多进程实现并发

看五、多进程服务

3.多线程服务器端

3.1 创建线程
#include <pthread.h>
/*
pthread_t* 传入一个该类型的地址
pthread_attr_t 设置线程的属性
第三个参数:传一个函数,返回类型为void*,参数为void*,
第四个参数:为函数传入参数
*/
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

//Compile and link with -pthread.
3.2 等待线程的消亡
#include <pthread.h>

/*
thread : 线程号
retval : 保存线程函数的返回值。
*/
int pthread_join(pthread_t thread, void **retval);
3. 3 互斥量
#include <pthread.h>

pthread_mutex_t;

int pthread_mutex_destroy(pthread_mutex_t *mutex);

//第二个参数为锁的属性,可以为NULL
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
              const pthread_mutexattr_t *restrict attr);
#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
3.4 信号量
#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value);

int sem_destroy(sem_t *sem);
//Link with -pthread.
  • sem : 创建信号量时传递保存信号量的变量地址值。
  • pshared :创建可由多个进程共享的信号量,传递0,创建只允许1个进程内部使用的信号量。
  • value :指定新创建的信号量初始值。
int sem_post(sem_t *sem);

int sem_wait(sem_t *sem);

int sem_trywait(sem_t *sem);

使用模板

sem_wait(&sem);
//临界区的开始
.....
//临界区的结束
sem_post(&se)

八、多种I/O函数

1.send(),recv()

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

ssize_t send(int sockfd, const void *buf, size_t len, 
             int flags);

ssize_t recv(int sockfd, void *buf, size_t len, 
             int flags);

flags

名称含义
MSG_OOB用于传输带外数据(发送紧急消息)
MSG_PEEK验证输入缓冲中是否存在接收的数据
MSG_DONTROUTE数据传输过程中不参照路由表在本地网络中寻找目的地
MSG_DONTWAIT调用I/O函数时不堵塞,用于非堵塞I/O
MSG_WITALL防止函数返回,直到接收全部请求的字节数

2.readv(),writev()

也就是说,通过writev函数可以将分散保存在多个缓冲中的数据一并发送,通过readv函数可以由多个缓冲分别接收。因此,适当使用这2个函数可以减少I/0函数的调用次数。

#include <sys/uio.h>

ssize_t readv(int fd, const struct iovec *iov, 
              int iovcnt);

ssize_t writev(int fd, const struct iovec *iov,
               int iovcnt);

struct iovec 
{
    void  *iov_base;    /* Starting address */
    size_t iov_len;     /* Number of bytes to transfer */
};

九、多播和广播

是基于UDP套接字实现。

1.设置TTL

在这里插入图片描述

2.加入多播组

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

3.广播

在这里插入图片描述

十、Linux编程

1.标准IO与系统IO之间的相互转化

#include <stdio.h>

//由fd转化为FILE指针类型
FILE *fdopen(int fd, const char *mode);

//由 FILE指针类型转化为fd
int fileno(FILE *stream)

2.复制文件描述符dup

#include <unistd.h>

int dup(int oldfd);
int dup2(int oldfd, int newfd);

3.实现半关闭

fd转化为FILE类型指针图。

在这里插入图片描述

通过复制文件描述符,实现半关闭。

在这里插入图片描述

在这里插入图片描述

调用shutdown,发送EOF,实现半关闭。


原文地址:https://blog.csdn.net/ccb1372098/article/details/142872169

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!