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

柳大的Linux讲义·基础篇(4)网络编程基础  

程序员文章站 2022-05-09 22:35:03
...

柳大的Linux游记·基础篇(4)网络编程基础

  • Author: 柳大·Poechant
  • Blog:Blog.CSDN.net/Poechant
  • Email:zhongchao.usytc#gmail.com (#->@)
  • Date:March 11th, 2012
  • Copyright © 柳大·Poechant(钟超·Michael)

回顾

  1. 《柳大的Linux游记·基础篇(1)磁盘与文件系统》
  2. 《柳大的Linux游记·基础篇(2)Linux文件系统的inode》
  3. 《柳大的Linux游记·基础篇(3)权限、链接与权限管理》

闲话

最近很忙,博文写的少。感谢一些博友的理解。

有博友发邮件说《柳大的Linux游记》希望继续写下去,希望了解一些与 socket 入门有关的内容。对此深表惭愧,当时也是应一个博友的来信而开始写这个系列的,但仅写了三篇就没继续了。与 socket 相关的文章,在网络上非常多,socket 编程也是基本功。要我来写的话,写出心意很难,我只希望能写系统一些,所以我想先介绍 socket 的基础,然后说说 select,poll 和 epoll 等 IO 复用技术,可能这样会系统一些,也更实用。

W. Richard StevensUNIX Network Programming Volume 1中讲解例子的时候都使用了include "unp.h",这是Stevens先生在随书源码中的提供的一个包含了所有 UNIX 网络编程会用到的头文件的的一个头文件。但这样对于不了解 UNIX 网络编程以及 socket 的朋友来说,并不是一个好的学习途径。所以我想看完本文后读Stevens先生的杰出作品更好一些 :)

另外,《JVM深入笔记》的第四篇正在整理,最近确实空闲时间比较少,对此感到很抱歉。我会尽量抽时间多分享一些的。

言归正传,下面还是沿袭我的一贯风格,先以最简单的实例开始。

目录

  1. 快速开始
    • 1.1 TCP C/S
      • 1.1.1 TCP Server
      • 1.1.2 TCP Client
    • 1.2. UCP C/S
      • 1.2.1 UDP Server
      • 1.2.2 UDP Client
  2. TCP 和 UCP 的 Socket 编程对比
    • 2.1 Server
    • 2.2 Client
    • 2.3 所使用的 API 对比
  3. 裸用 socket 的性能很差

1 快速开始

1.1 TCP C/S

无论你是使用 Windows 还是 UNIX-like 的系统,操作系统提供给应用层的网络编程接口都是 Socket。在 5 层的 TCP/IP 网络结构或者 7 层的 OSI 网络结构中,都有传输层,TCP 和 UDP 协议就是为传输层服务的。而网络层的最常用协议就是 IP(IPv4 或 IPv6)在高层编写程序,就需要用到 TCP 协议和 UDP 协议。其直接使用,就是通过 Socket 来实现的。

先看一段简单的 TCP 通信的 Server 与 Client 例程。

1.1.1 TCP Server

下面是一个 TCP 连接的 Server 实例。

#include <string.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    // Get Port option
    if (argc < 2)
    {
        fprintf(stderr, "ERROR, no port provided\n");
        exit(1);
    }
    int port_no = atoi(argv[1]);

    // Get socket
    int socket_fd = socket(AF_INET, SOCK_STREAM, 0);

    // Bind
    struct sockaddr_in server_addr;
    bzero((char *) &server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(port_no);
    bind(socket_fd, (struct sockaddr *) &server_addr, sizeof(server_addr));

    // Listen
    listen(socket_fd, 5);

    while (1) {

        // Accept
        struct sockaddr_in client_addr;
        socklen_t client_addr_length = sizeof(client_addr);
        int socket_to_client = accept(socket_fd, (struct sockaddr *) &client_addr, &client_addr_length);

        // Read
        char buffer[1024];
        bzero(buffer, sizeof(buffer));
        read(socket_to_client, buffer, sizeof(buffer) - 1);
        printf("Here is the message: %s\n", buffer);

        // Write
        const char *data = "I got your message.";
        write(socket_to_client, data, sizeof(data));

        // Close
        close(socket_to_client);
    }

    close(socket_fd);

    return 0;
}

上面是 TCP 的 Client 的 Simplest Example。概括起来 Scoket Server 编程有如下几个步骤:

1.1.1.1 获取 Socket Descriptor:
    // socket function is included in sys/socket.h
    // AF_INET is included in sys/socket.h
    // SOCK_STREAM is included in sys/socket.h
    socket(AF_INET, SOCK_STREAM, 0);

通过sys/socket.h中的socket函数。第一个参数表示使用IPv4 Internet Protocol,如果是AF_INET6则表示IPv6 Internet Protocol,其中AF表示Address Family,另外还有PF表示Protocol Family。第二个参数表示流传输Socket Stream,流传输是序列化的、可靠的、双向的、面向连接的,Kernel.org 给出的解释是:“Provides sequenced, reliable, two-way, connection-based byte streams. An out-of-band data transmission mechanism may be supported.”

另外一个常用的是SOCK_DGRAM表示Socket Diagram,是无连接的、不可靠的传输方式,Kernel.org 给出的解释是“Supports datagrams (connectionless, unreliable messages of a fixed maximum length).”

第三个参数表示使用的协议族中的哪个协议。一般来说一个协议族经常只有一个协议,所以长使用“0”。具体参见Kernel.org 给出的解释

1.1.1.2 绑定地址与端口

首先要创建一个struct sockaddr_in,并设置地址族、监听的外来地址与本地端口号。如下:

// struct sockaddr_in is inclued in netinet/in.h
// bzero function is included in string.h
// atoi is include in stdlib.h
struct sockaddr_in server_addr;
bzero((char *) &server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(atoi(argv[1]))

然后将第 1 步创建的Socket与这里创建的地址绑定(实际上直接用的是Socket Descriptor)。

// struct sockaddr is included in sys/socket.h
// bind function is included in sys/socket.h
bind(socket_fd, (struct sockaddr *) &server_addr, sizeof(server_addr));
1.1.1.3 开始监听
// listen function is included in sys/socket.h
    listen(socket_fd, 5);

第一个参数不用赘述了,第二个参数是连接队列(connection queue)的最大长度。当Server进行了Accept后,在新来的请求就得进入队列等待,如果队列满了,再来的连接就会被拒绝。

1.1.1.4 接受连接
// struct sockaddr_in is included in netinet/in.h
// accept function is included in sys/socket.h
struct sockaddr_in client_addr;
socklen_t client_addr_length = sizeof(client_addr);
int socket_to_client = accept(socket_fd, (struct sockaddr *) &client_addr, &client_addr_length);

在开始监听socket_fd后,接收来自该Socket的连接,将获取到的客户端地址和地址长度写入client_addrclient_addr_length中。该accept在成功接受某连接后会得到该连接的Socket,并将其Socket Descriptor返回,就得到了socket_to_client

1.1.1.5 接收和发送数据
// bzero function is included in string.h
// read function is included in unistd.h
char buffer[1024];
bzero(buffer, sizeof(buffer));
read(socket_to_client, buffer, sizeof(buffer) - 1);

在接受连接后,就可以从该Socket读取客户端发送来的数据了,数据读取到char *的字符串中。发送过程也类似。

// write function is included in unistd.h
const char *data = "Server has received your message.";
write(socket_to_client, data, sizeof(data));
1.1.1.6 关闭Socket
// close function is included in unistd.h
close(socket_fd);

以上就简单解释了 TCP Server 的 Socket 通信过程。简单概括如下:

Create Socket - Bind socket with port - Listen socket - Accept connection - Read/Write - Close

1.1.2 TCP Client

再来看看 Client。以下是例程:

#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <netdb.h> // bcopy
#include <stdlib.h> 

int main(int argc, char *argv[])
{
    // Get options
    if (argc < 3)
    {
        fprintf(stderr, "Usage: %s <hostname> <port>\n", argv[0]);
        exit(1);
    }
    struct hostent *server_host = gethostbyname(argv[1]);
    int server_port = atoi(argv[2]);

    // Get socket
    int socket_fd = socket(AF_INET, SOCK_STREAM, 0);

    // Connect
    struct sockaddr_in server_addr;
    bzero((char *) &server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    bcopy((char *) server_host->h_addr, (char *) &server_addr.sin_addr.s_addr, server_host->h_length);
    server_addr.sin_port = htons(server_port);
    connect(socket_fd, (struct sockaddr *) &server_addr, sizeof(server_addr));

    // Input
    printf("Please enter the message: ");
    char buffer[1024];
    bzero(buffer, sizeof(buffer));
    fgets(buffer, sizeof(buffer) - 1, stdin);

    // Write
    write(socket_fd, buffer, strlen(buffer));

    // Read
    bzero(buffer, sizeof(buffer));
    read(socket_fd, buffer, sizeof(buffer) - 1);
    printf("%s\n", buffer);

    // Close
    close(socket_fd);

    return 0;
}

上面是 TCP 的 Client 的 Simplest Example。概括起来 Scoket Client 编程有如下几个步骤:

1.1.2.1 获取 Socket Descriptor:

与 Server 一样。

1.1.2.2 连接服务器
// struct sockaddr_in is included in netinet/in.h
// struct sockaddr is included in sys/socket.h
// bzero is included in string.h
// bcopy is included in string.h
// AF_INET is included in sys/socket.h
// connect is included in sys/socket.h
connect(socket_fd, (struct sockaddr *) &server_addr, sizeof(server_addr));

第一个参数是int型的Socket Descriptor,通过socket函数得到。第二个参数是指向struct sockaddr地址的指针,第三个参数是该地址的大小。而这个地址是通过struct sockaddr_in得到的,然后用bzero位初始化,再赋初值,包括地址族为AF_INET,地址为struct sockaddr_in中的h_addr,这里用到了bcopy位拷贝函数,最后再赋上端口号htons(int server_port)connect的作用,就是将本地的Socket与服务器建立连接,而这个Socket则是通过Socket Descriptor来标示的。

1.1.2.3 发送或接收数据

首先看发送数据:

// write function is included in unistd.h
char buffer[1024]
...
write(socket_fd, buffer, strlen(buffer));

然后用write函数,向Socket所连接的服务器发送数据,数据是char *的字符串。再看下面的接收数据:

// read function is included in unistd.h
read(socket_fd, buffer, sizeof(buffer) - 1);

第一个参数是Socket Descriptor,第二个参数是char *的字符串,长度为第三个参数标示的sizeof(buffer)-1。功能就是从socket_fd标示的Socket所连接的服务器读取数据。

1.1.2.4 关闭Socket
// close function is included in unistd.h
close(socket_fd);

以上就简单解释了客户端的最基本的Socket通信。概括起来的过程就是:

Create Socket - Connect socket with server - Write/Read - Close

1.2 UDP C/S

刚才介绍了最简单的 TCP C/S 模型,下面看看 UDP C/S 模型。

1.2.1 UDP Server

下面是 UDP 连接的 Server 实例:

#include "sys/socket.h"
#include "netinet/in.h"
#include "string.h"

int main(int argc, char *argv[])
{
    // Create Socket
    int socket_fd = socket(AF_INET, SOCK_DGRAM, 0);

    // Bind
    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(atoi(argv[1]));
    bind(socket_fd, (struct sockaddr *) &server_addr, sizeof(server_addr));

    // As accept
    while (1)
    {
        // Receive
        char buffer[1024];
        struct sockaddr_in client_addr;
        socklen_t client_addr_length = sizeof(client_addr);
        int msg_recv_length = recvfrom(socket_fd, buffer, sizeof(buffer), 0, (struct sockaddr *) &client_addr, &client_addr_length);

        // Write
        const char *msg_send = "Received a datagram: ";
        write(1, msg_send, strlen(msg_send));
        write(1, buffer, msg_recv_length);

        // Send
        const char *msg_send_2 = "Got your message\n";
        sendto(socket_fd, msg_send_2, strlen(msg_send_2), 0, (struct sockaddr *) &client_addr, sizeof(struct sockaddr_in));
    }

    close(socket_fd);

    return 0;
}

UDP Server 的建立主要有以下几步:

1.2.1.1 获取 Socket Descriptor
sock=socket(AF_INET, SOCK_DGRAM, 0);

创建Socket,获取Socket Descriptor

1.2.1.2 绑定地址与端口
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(atoi(argv[1]));
bind(socket_fd, (struct sockaddr *) &server_addr, sizeof(server_addr));

将端口和允许的地址与Socket绑定。

1.2.1.3 接收和发送数据
char buffer[1024];
struct sockaddr_in client_addr;
socklen_t client_addr_length = sizeof(client_addr);
int msg_recv_length = recvfrom(socket_fd, buffer, sizeof(buffer), 0, (struct sockaddr *) &client_addr, &client_addr_length);

接收数据用的是sys/socket.h中的recvfrom函数,第一个参数是Socket Descriptor,第二个参数是用于存储数据的buffer,第三个参数是数据存储区域的长度(注,对于数组用sizeof取到的值是数组长度乘以长度;对于指针用sizeof取到的值是指针长度,对于32位机是4,对于64位机是8)。第四个参数是标志符,一般设置为0,具体可以查看info recvfrom。第五个参数用于存储客户端的地址,第六个参数是存储客户端地址的socklen_t型变量的长度。

用起来也很好记(也可以现查现用),先是套接字,然后是存储区及其大小,接着是标志符,最后是客户端地址及其大小。

const char *msg_send_2 = "Got your message\n";
sendto(socket_fd, msg_send_2, strlen(msg_send_2), 0, (struct sockaddr *) &client_addr, sizeof(struct sockaddr_in));

发送数据用更多是sys/socket.h中的sendto函数,第一个参数Socket Descriptor,第二和第三个参数是所发送的数据及其大小,然后是标示符(一般为0),最后是客户端地址及其大小。

1.2.1.4 关闭Socket
close(socket_fd);

简单概括一下 UDP Server 的 Socket 编程步骤:

Create Socket - Bind socket with port - Recv/Send - Close

1.2.2 UDP Client

#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <netdb.h>

int main(int argc, char *argv[])
{
    // Create socket
    int socket_fd = socket(AF_INET, SOCK_DGRAM, 0);

    // Initialize server address
    struct hostent *server_host = gethostbyname(argv[1]);
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET; // socket internet family
    bcopy((char *)server_host->h_addr, (char *)&server_addr.sin_addr.s_addr, server_host->h_length); // socket internet address
    server_addr.sin_port = htons(atoi(argv[2])); // socket internet port

    // Send
    char buffer[1024];
    sendto(socket_fd, buffer, sizeof(buffer), 0, (const struct sockaddr *) &server_addr, sizeof(struct sockaddr_in));

    // Receive and write
    struct sockaddr_in client_addr;
    socklen_t client_addr_length = sizeof(client_addr);
    int msg_recv_length = recvfrom(socket_fd, buffer, sizeof(buffer), 0, (struct sockaddr *) &client_addr, &client_addr_length);
    const char *hint = "Got an ACK: ";
    write(1, hint, strlen(hint));
    write(1, buffer, msg_recv_length);

    // Close
    close(socket_fd);

    return 0;
}
1.2.2.1 创建Socket
int socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
1.2.2.2 发送和接收数据
sendto(socket_fd, buffer, sizeof(buffer), 0, (const struct sockaddr *) &server_addr, sizeof(struct sockaddr_in));

与 UDP Server 的 sendto 一样。注意初始化。

1.2.2.3 关闭Socket
close(socket_fd);

总结一下 UDP Client 编程的几个步骤:

Create Socket - Send/Receive - Close

2 图解 TCP 和 UDP 原理

声明:图片来自此处

柳大的Linux讲义·基础篇(4)网络编程基础
            
    
    
         

上图为 TCP 原理图示

柳大的Linux讲义·基础篇(4)网络编程基础
            
    
    
         

上图为 UDP 原理图示

3 TCP 和 UCP 的 Socket 编程对比

3.1 Server

TCP 的过程是:

  1. Create server socket
  2. Bind the server socket with client addresses and a local server port
  3. Listen the server socket
  4. Accept with blocking, until the connection has established and then get a client socket
  5. Read and write data from the client socket
  6. Close the client socket as discontecting
  7. Close the local server socket

UDP 的过程是:

  1. Create server socket
  2. Bind the server socket with client addresses and a local server port
  3. Receive data and client address through the server socket
  4. Send data to the client address through the server socket
  5. Close the local server socket

通过对比,我们可以看到,相同点如下:

  1. 一开始都要创建 socket
  2. 接着都要绑定 socket 与本地端口和指定的客户端地址
  3. 最后都要关闭本地 socket

不过这些相似点似乎没什么价值,还是看看不同点吧。

  1. TCP 要监听端口,然后阻塞式地等待连接;UDP 则通过自身的循环来不断读取,不阻塞也不建立连接。
  2. TCP 建立连接后会有一个 client socket,然后通过向这个 socket 的读写实现数据传输;UDP 则直接向客户端地址发送和接收数据。
  3. 因为 TCP 方式有 client socket,所以完成一次传输后,可以关闭 client socket。当然也可以一直连着不关闭。

可以看到,TCP 和 UDP 的本质区别就是面向连接还是无连接的。因为面向连接,所以要监听到是否有 connection 到来,connection 一旦到来,就阻塞住,然后会有一个 socket 跳出来作为代言。通过对这个 socket 的读写就实现了对 connection 的另一端的客户端的读写。

3.2 Client

TCP 的过程是:

  1. Create client socket
  2. Connect the server address and port with the client socket
  3. After connection is established, read and write data to the client socket
  4. Close the local socket socket

UDP 的过程是:

  1. Create client socket
  2. Send data to the server address through the client socket
  3. Receive data and the server address throught the client socket
  4. Close the local client socket

可以看到如下区别:

  1. TCP 方式要 connect 服务器地址/端口与 socket;UDP 则不需要这个过程。
  2. TCP 方式在 connection 建立后,通过 client socket 读写数据;而 UDP 方式则直接通过 client socket 向服务器地址发送数据。

3.3 所使用的 API 对比

TCP 方式的 Server 用到:

  • socket
  • bindlisten
  • accept
  • readwrite
  • close

UDP 方式的 Server 用到:

  • sokect
  • bind
  • recvfromsendto
  • close

TCP 方式的 Client 用到:

  • socket
  • connect
  • writeread
  • close

UDP 方式的 Client 用到:

  • socket
  • sendtorecvfrom
  • close

4 裸用 socket 的性能很差

是的,这是最传统的网络编程方式:“One traditional way to write network servers is to have the main server block on accept(), waiting for a connection. Once a connection comes in, the server fork()s, the child process handles the connection and the main server is able to service new incoming requests.”

下一篇,我会介绍 IO 复用技术中在 Linux 下常用的 select、poll 和 epoll。

5 参考

  1. http://www.lowtek.com/sockets/select.html
  2. http://www.kernel.org/doc/man-pages/online/pages/man7/socket.7.html
  3. http://www.kernel.org/doc/man-pages/online/pages/man2/listen.2.html
  4. http://www.kernel.org/doc/man-pages/online/pages/man2/send.2.html
  5. http://www.kernel.org/doc/man-pages/online/pages/man2/sendto.2.html
  6. http://www.kernel.org/doc/man-pages/online/pages/man2/recv.2.html
  7. http://www.kernel.org/doc/man-pages/online/pages/man2/recvfrom.2.html
  8. http://www.linuxhowtos.org/C_C++/socket.htm

-

Happy Coding, enjoy sharing!

转载请注明来自“柳大的CSDN博客”:Blog.CSDN.net/Poechant

-