APUE学习之socket网络编程

目录

一、socket通信简介

二、socket通信的基本流程

三、socket服务器和客户端示例代码

1、服务端

2、客户端

3、运行结果

四、socket编程函数详解

1、socket()函数

2、bind()函数 

 3、网络字节序和主机字节序

4、listen()函数

5、accept()函数

6、connect()函数

7、close()、shutdown()函数


一、socket通信简介

Socket通信是一种计算机网络中常见的通信机制,用于在不同计算机之间进行数据交换。Socket(套接字)是网络编程的基本组件之一,它提供了一种标准的编程接口,允许应用程序通过网络进行通信。

下面是Socket通信的一些基本概念和要点:

  1. 套接字(Socket): 套接字是通信的基本单元,它是网络通信的接口。套接字提供了一种封装了网络通信细节的抽象层,使得应用程序可以通过套接字进行数据的发送和接收。

  2. 通信协议: Socket通信可以使用不同的通信协议,其中最常见的是TCP(传输控制协议)和UDP(用户数据报协议)。TCP提供可靠的、面向连接的通信,而UDP是一种无连接的、不可靠的通信方式。

  3. 服务器端和客户端: 在Socket通信中,通常涉及两个主要角色:服务器端和客户端。服务器端等待并响应连接请求,而客户端发起连接请求并与服务器端建立连接。

  4. 地址和端口: 在Socket通信中,每个主机都有一个唯一的IP地址,而每个正在运行的应用程序则有一个端口号。通信双方通过IP地址和端口号来确定彼此的位置。

  5. 流程: Socket通信的基本流程包括创建套接字、绑定地址和端口、监听连接请求(对于服务器端)、接受连接请求(对于服务器端)、建立连接(对于客户端)、发送和接收数据、关闭连接等步骤。

  6. 阻塞和非阻塞: Socket通信可以是阻塞的或非阻塞的。在阻塞模式下,某些操作(如接受连接或发送数据)会导致程序阻塞,直到操作完成。非阻塞模式允许程序在等待操作完成时继续执行其他任务。

  7. 多线程和多进程: 在服务器端,可以使用多线程或多进程来处理多个连接,实现并发处理。每个连接都在独立的线程或进程中进行处理,以避免阻塞整个服务器。

Socket通信在网络编程中具有广泛的应用,例如Web服务器、聊天应用、文件传输等。它提供了一种灵活而强大的方式,使得不同计算机之间可以进行可靠的数据交换。

二、socket通信的基本流程

APUE学习之socket网络编程_第1张图片

三、socket服务器和客户端示例代码

1、服务端

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 


#define LISTEN_PORT 8889            //监听端口设为8889
#define BACKLOG     13             //相应socket可以在内核里排队的最大连接个数



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


        int                     rv        = -1;
        int                     listen_fd = -1;
        int                     client_fd = -1;
        struct sockaddr_in      ser_addr;
        struct sockaddr_in      cli_addr;
        socklen_t               cliaddr_len = sizeof(cli_addr);
        char                    buf[1024];

        listen_fd = socket(AF_INET,SOCK_STREAM,0);
        if(listen_fd < 0)
        {

                printf("create socket failure : %s\n",strerror(errno));
                return -1;

        }
        printf("socket create fd[%d]\n",listen_fd);

        memset(&ser_addr,0,sizeof(ser_addr));
        ser_addr.sin_family      = AF_INET;
        ser_addr.sin_port        = htons(LISTEN_PORT);
        ser_addr.sin_addr.s_addr= htonl(INADDR_ANY);        //意味着监听所有的IP地址
        if(bind(listen_fd,(struct sockaddr*)&ser_addr,sizeof(ser_addr)) < 0)
        {

                printf("create socket failure : %s\n",strerror(errno));
                return -2;
        }
        printf("socket[%d] bind on port[%d] for all IP address ok\n",listen_fd,LISTEN_PORT);

        listen(listen_fd,BACKLOG);

        while(1)
        {

                printf("\nStart waiting and accept new client connect.....\n",listen_fd);

                client_fd = accept(listen_fd,(struct sockaddr*)&cli_addr,&cliaddr_len);
                if(client_fd < 0)
                {

                        printf("accept new socket failure : %s\n",strerror(errno));
                        return -3;

                }
                printf("Accept new client[%s:%d]with fd [%d]\n",inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port),client_fd);

                memset(buf,0,sizeof(buf));
                if((rv = read(client_fd,buf,sizeof(buf))) < 0)
                {


                        printf("Read data from client socket[%d] failure : %s\n",client_fd,strerror(errno));
                        close(client_fd);
                        continue;

                }
                else if(rv == 0)
                {

                        printf("client socket[%d]disconnected\n",client_fd);
                        close(client_fd);
                        continue;

                }
                else if(rv > 0)
                {
                        printf("read %d bytes data from client[%d] and echo it back :'%s'\n",rv,client_fd,buf);

                }

                if(write(client_fd,buf,rv) < 0)
                {

                        printf("Write %d bytes data back to client[%d] failure : %s\n",rv,client_fd,strerror(errno));
                        close(client_fd);

                }

                sleep(1);

                close(client_fd);
        }
        close(listen_fd);

}

2、客户端

#include           
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define SERVER_IP       "127.0.0.1"
#define SERVER_PORT     8889
#define MSG_STR         "Hello,Unix Network Program World!"

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


        int     con_fd          = -1;
        int     rv              = -1;
        struct sockaddr_in      ser_addr;
        char                    buf[1024];

        con_fd = socket(AF_INET,SOCK_STREAM,0);
        if(con_fd < 0)
        {

                printf("create socket failure : %s\n",strerror(errno));
                return -1;
        }

        memset(&ser_addr,0,sizeof(ser_addr));
        ser_addr.sin_family = AF_INET;
        ser_addr.sin_port   = htons(SERVER_PORT);
        inet_aton(SERVER_IP,&ser_addr.sin_addr);

        if(connect(con_fd,(struct sockaddr*)&ser_addr,sizeof(ser_addr)) < 0)
        {

                printf("connect to server [%s:%d] failure :%s\n",SERVER_IP,SERVER_PORT,strerror(errno));
                return -2;

        }

        if(write(con_fd,MSG_STR,strlen(MSG_STR)) < 0)
        {

                printf("Write data to server failure : %s\n",strerror(errno));
                goto cleanup;

        }

        memset(buf,0,sizeof(buf));
        if((rv = read(con_fd,buf,sizeof(buf))) < 0)
        {
                printf("Read data from server failure :%s\n",strerror(errno));
                goto cleanup;

        }
        else if(rv == 0)
        {

                printf("client connect to server failure get disconnected\n");
                goto cleanup;

        }
        printf("Read %d bytes data from server:'%s'\n",rv,buf);

cleanup:
        close(con_fd);

}

3、运行结果

APUE学习之socket网络编程_第2张图片

四、socket编程函数详解

1、socket()函数

int socket(int domain, int type, int protocol);

 socket() 函数是在Socket编程中用于创建套接字的函数,他会创建一个socket描述符,它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作有用到它,接下来叫我们来看看socket()函数的三个参数吧!

domain:即协议域,又称为协议族(family),常见的协议族有AF_INET、AF_INET6、AF_LOCAL,协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位)与端口号(16位)的组合。

type:指定socket类型。常用的有 SOCK_STREAM(流套接字,使用 TCP 协议)和 SOCK_DGRAM(数据报套接字,使用 UDP 协议)。

protocol:指定协议,通常可设为0.当protocol为0时,会自动选择type类型对应的默认协议。

2、bind()函数 

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

 bind()函数用于将一个套接字(socket)与一个特定的本地地址(通常是IP地址和端口号)关联起来。这个函数通常在服务器端创建套接字后,用于绑定套接字到一个具体的网络地址,以便监听来自该地址的连接请求。

接下来叫我们看看bind()函数的三个参数吧:

sockfd:即通过socket()函数创建的套接字的文件描述符。

addlen:对应的是地址的长度。 

addr:一个struct sockaddr结构体的指针,包含要绑定的地址信息。这个地址结构根据地址创建socket时的协议族的不同而不同,但最终都会强制转换后赋值给sockaddr这种类型的指针给内核。

ipv4对应的sockaddr_in类型定义(在头文件netinet/in.h中):

struct sockaddr_in {


    unsigned short              sin_family;          // 地址族,通常设置为 AF_INET
    uint16_t                         sin_port;           // 端口号,网络字节序(Network Byte Order)
    struct in_addr                sin_addr;       // IPv4地址结构,包含一个32位的IPv4地址
    unsigned char sin_zero[8];             // 用于填充,保证结构体大小与 struct sockaddr 相同


};

其中,struct in_addr是一个包含32位IPv4地址的结构体,定义如下: 

struct in_addr {

        in_addr_t                 s_addr; // 存储32位IPv4地址

};

 3、网络字节序和主机字节序

网络字节序(Network Byte Order)和主机字节序(Host Byte Order)是两种不同的字节序(Byte Order)表示方式,它们在计算机网络通信中起到了重要的作用。

1. 网络字节序(Network Byte Order):
   - 网络字节序是一种标准的字节序,用于在网络上传输数据。在网络字节序中,数据的高字节(Most Significant Byte,MSB)存储在内存的低地址处,而低字节(Least Significant Byte,LSB)存储在内存的高地址处。网络字节序是大端字节序(Big-Endian)的一种表示方式。
   - 在网络通信中,为了保证不同计算机之间的数据交换的正确性,通常都使用网络字节序进行数据的表示和传输。

2. 主机字节序(Host Byte Order):
   - 主机字节序是指在特定计算机体系结构中使用的字节序。不同计算机体系结构采用不同的字节序,其中有些是大端字节序,有些是小端字节序(Little-Endian)。
   - 大部分个人计算机(如x86架构)使用小端字节序,即低字节存储在内存的低地址处。而一些其他体系结构(如PowerPC)使用大端字节序,即高字节存储在内存的低地址处。

在网络编程中,为了确保在不同计算机体系结构之间传输数据时不发生混淆,通常需要进行字节序的转换。函数 `htonl()`、`htons()`、`ntohl()`、`ntohs()` 是常用的字节序转换函数,它们分别表示将32位和16位的整数从主机字节序转换为网络字节序,以及从网络字节序转换为主机字节序。

- `htonl()`:Host to Network Long
- `htons()`:Host to Network Short
- `ntohl()`:Network to Host Long
- `ntohs()`:Network to Host Short

这些函数帮助确保在进行网络通信时,数据的字节序是符合网络字节序标准的,从而保证数据的正确传输。

4、listen()函数

listen() 函数用于使套接字处于监听状态,等待客户端的连接请求。具体来说,listen() 函数的作用是告诉操作系统,套接字已经准备好接受来自客户端的连接请求,并设置套接字的状态为监听状态。

int listen(int sockfd, int backlog);

 sockfd:即通过socket()函数创建的套接字的文件描述符。

backlog:相应的socket可以在内核里排队的最大连接个数。

5、accept()函数

accept() 函数用于接受客户端的连接请求,创建一个新的套接字来处理与客户端的通信。具体来说,accept() 函数会等待客户端发起连接请求,一旦有连接请求到达,它会创建一个新的套接字用于与该客户端进行通信,而原始的套接字仍然处于监听状态,可以继续接受其他连接请求。

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

sockfd:监听套接字的文件描述符,即通过 socket()bind() 函数创建并绑定的套接字.

*addr :用于返回客户端的协议地址,这个地址里包含有客户端的IP和端口信息等。

addlen:返回客户端协议地址的长度。

accept() 函数的调用会阻塞程序的执行,直到有客户端连接请求到达。一旦有连接请求到达,accept() 的返回值是由内核自动生成的一个全新的描述字(fd),代表与返回客户的TCP连接。如果想发送数据给该客户端,则我们可以调用write()等函数往该fd里写内容即可;而如果想从该客户端读内容则调用read()函数从该fd里读数据即可。一个服务器通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务区进程接受的客户连接创建了一个新的socket描述字,当服务器完成了对某个客户的服务,就应该把客户端相应的socket描述字关闭。

accept()系统调用将会把客户端的信息保存在cli_addr这个结构体变量中,我们知道cli_addr是struct sockaddr_in这种IPV4地址类型,客户端的IP地址和端口号都保存在该结构体中。当然在该结构体中IP地址是以32位整形值的形式存放,端口号也是以网络字节序的形式存放的。这时我们可以使用inet_ntoa()函数将32位整形的IP地址转换成点分十进制字符串格式的IP地址“127.0.0.1”,我们也可以调用ntohs()函数将网络字节序的端口号转换成主机字节序的端口号。

6、connect()函数

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

connect()函数用于与服务器建立连接。如果客户端这时调用connect发出连接请求,服务器端就会接收到这个请求并使accept()返回,accept()返回的新的文件描述符就是对应到客户的TCP连接,通过两个文件描述符(客户端connect的fd和服务端accept返回的fd)就可以实现客户端和服务端的相互通信。

IP地址“127.0.0.1”这是点分十进制形式的字符串形式,而在结构体struct sockaddr_in中IP地址是以32位数据保存的,这时我们可以调用inet_aton()函数将点分十进制字符串转换成32位整形类型。同样,端口号也要使用htons()函数从主机字节序转换成网络字节序。

7、close()、shutdown()函数

在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字。close一个TCP socket的缺省行为时把该socket标记为已关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用。

int close(int fd);

如果对socket fd调用close()则会触发该TCP连接断开的四次握手,有些时候我们需要数据发送出去之后并到达对方之后才能关闭socket套接字,则可以调用shutdown()函数来半关闭套接字:

int shutdown(int sockfd, int how);

如果how的值为SHUT_RD,则该套接字不可再读入数据了;如果how的值为SHUT_WR,则该套接字不可在发送数据了;如果how的值为SHUT_RDWR,则该套接字既不可读也不可写数据了。

你可能感兴趣的:(APUE,linux,网络,服务器)