初试linux下的Socket通信(上)

网络之间如何通信

通信首要解决的问题是如何唯一标识一个进程。在本地通信中,可以通过进程PID表示唯一标识一个进程,但在网络通信是不可行的。网络通信通常由两部分组成:服务器和客户端。一个服务器将同时跟多个客户端通信,要实现这种服务器与客户端之间的交互通信,那就需要一个协议(protocol)。互联网通信有很多通信,一部分是低层协议(low-level protocols),比如是IP协议(Internet Protocol, 网际协议),用来控制二进制0,1数据在网络中的发送方式;一个是高层协议(high-level protocols),比如是HTTP协议(Hypertext Transfer Protocol, 超文本传输协议),用来控制浏览器与网络服务器之间的交互通信。

为了解决网络之间的通信,TCP/IP就应运而生。TCP/IP不是一种协议,而是一个协议族的统称。里面包括了IP协议,IMCP协议,TCP协议,以及我们更加熟悉的http、ftp、pop3协议等等。网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。下图是TCP/IP协议族的结构图(图片来自http://www.cnblogs.com/goodcandle/archive/2005/12/10/socket.html#!comments
这里写图片描述
TCP/IP协议族按照层次由上到下,层层包装。最上面的就是应用层了,这里面有http,ftp,等等我们熟悉的协议。而第二层则是传输层,著名的TCP和UDP协议就在这个层次。第三层是网络层,IP协议就在这里,它负责对数据加上IP地址和其他的数据以确定传输的目标。第四层是叫数据链路层,这个层次为待传送的数据加入一个以太网协议头,并进行CRC编码,为最后的数据传输做准备。再往下则是硬件层次了,负责网络的传输,这个层次的定义包括网线的制式,网卡的定义等等,所以有些书并不把这个层次放在tcp/ip协议族里面,因为它几乎和tcp/ip协议的编写者没有任何的关系。发送协议的主机从上自下将数据按照协议封装,而接收数据的主机则按照协议从得到的数据包解开,最后拿到需要的数据。这种结构非常有栈的味道,所以某些文章也把tcp/ip协议族称为tcp/ip协议栈。

一切皆Socket

说好的Socket通信的呢?由上图可以看出,Socket位于应用层与传输层之间,是他们之间的中间软件抽象层。负责应用程序与底层协议之间的通信,类似与接口函数。就像Unix/Linux用“打开open –> 读写write/read –> 关闭close”模式去操作底层一样。下面这张图很好地阐述了Socket的基本操作,并且很清楚地表达了客户端与服务器是如何地通信。(图片来自http://www.cnblogs.com/goodcandle/archive/2005/12/10/socket.html#!comments
这里写图片描述
服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。

服务器的Socket通信

服务器使用Socket与客户端通信前,必须经过四部曲,简称为BLAB,分别是:绑定(Bind),监听(Listen),接收(Accept)和开始(Begin)。关于接口函数的参数意义可以参照这篇博文,写得非常详细。http://www.cnblogs.com/skynet/archive/2010/12/12/1903949.html

#include <sys/socket.h>
...
int listener_d = socket(PF_INET, SOCK_STREAM, 0); // 创建套接字
if (listener_d == -1)
    error("socekt error");
...
1. 绑定接口

计算机可能要同时运行多个服务器程序,比如发送网页,发送邮件,聊天等等。每个任务都需要使用不同端口(port)。所以在服务器启动时,必须告诉操作系统跟哪个端口通信,这个过程就是端口绑定。

  • 端口号的范围是0~65535,一般情况下,只有超级用户或管理员才能使用1024以下的端口,比如网页服务器的80端口。
#include <arpa/inet.h>
...
struct sockaddr_in name;
name.sin_family = PF_INET;
name.sin_port = (in_port_t) htons(30000);
name.sin_addr.s_addr = htonl(INADDR_ANY);
int c = bind(listener_d, (struct sockaddr *)&name, sizeof(name)); // 把套接字绑定到30000端口
if (c == -1)
    error("bind error");
...
2. 监听

服务器启动之后,可能会有很多客户端同时连接,使用listen( )接口函数告知操作系统最多可以有多少个客户端同时尝试连接服务器,并且不会立即得到响应,需要排队等候。多于这个数的客户端就会被告知服务器太忙。

if (listen(listener_d, 10) == -1)     //监听长度队列为10
    error("listen error");
3. 接受连接

服务器端依次调用socket()、bind()、listen()之后,服务器开始等待客户端来连接。调用accept( )接口函数等待,直到有客户端连接时,如果成功连接,函数会返回一个全新的Socket描述字,一个服务器通常通常只创建一个监听socket描述字,内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。

...
struct sockaddr_storage client_addr;   //client_addr 保存连接客户端的详细信息
unsigned int address_size = sizeof(client_addr);
int connect_d = accept(listener_d, (struct sockaddr *)&client_addr, &address_size); //接收来自客户端的连接
if (connect_d == -1)
    error("accept error");
...
4. 服务器简单测试

下面的程序实现简要的服务器功能,为已连接的客户端随机发送字符串。客户端我们直接用telnet来测试

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

void error(char *msg)
{
    fprintf(stderr, "%s :  %s\n", msg, strerror(errno) );
    exit(1);
}

int main(int argc, char const *argv[])
{
    char *advice[] = {
        "Take smaller bites\r\n",
        "Go for the tight jeans. No they do NOT make you look fat.\r\n",
        "One word: inappropriate\r\n",
        "Just for today, be honest. Tell your boss what you *really* think\r\n",
        "You might want to rethink that haircut\r\n"
    };

    int listener_d = socket(PF_INET, SOCK_STREAM, 0); // 创建套接字
    if (listener_d == -1)
        error("socekt error");

    struct sockaddr_in name;
    name.sin_family = PF_INET;
    name.sin_port = (in_port_t) htons(30000);
    name.sin_addr.s_addr = htonl(INADDR_ANY);
    int c = bind(listener_d, (struct sockaddr *)&name, sizeof(name)); // 把套接字绑定到30000端口
    if (c == -1)
        error("bind error");

    if (listen(listener_d, 10) == -1)     //监听长度队列为10
        error("listen error");
    puts("waiting for connection");

    while (1)
    {
        struct sockaddr_storage client_addr;   //client_addr 保存连接客户端的详细信息
        unsigned int address_size = sizeof(client_addr);
        int connect_d = accept(listener_d, (struct sockaddr *)&client_addr, &address_size); //接收来自客户端的连接
        if (connect_d == -1)
            error("accept error");

        char *msg  = advice[rand( ) % 5];

        if ( send(connect_d, msg, strlen(msg), 0) == -1)
            error("send error");
        close(connect_d);
    }

    return 0;
}

编译代码,运行程序,可以看到服务器正在等待连接
这里写图片描述
新开一个终端,用telnet连接服务器那个端口,然后就会接收到服务器发送的字符串。说明服务器正常工作。
这里写图片描述
当服务器已经响应某个客户端时强制关闭了服务器,立即重启后,bind接口函数会调用失败。
这里写图片描述
这是因为当你某个端口绑定了Socket,操作系统在30s内不会允许程序再次绑定。需要下面的代码对Socket设置下

int reuse = 1;
if (setsockopt(listener_d, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(int)) == -1)
    error("Can't set the reuse option on the socket");

参考博文

  1. 揭开Socket编程的面纱 http://www.cnblogs.com/goodcandle/archive/2005/12/10/socket.html#!comments
  2. 图解TCP-IP协议 http://www.cricode.com/3568.html
  3. TCP/IP详解学习笔记 http://blog.csdn.net/goodboy1881/article/category/204448
  4. Linux Socket编程(不限Linux) http://www.cnblogs.com/skynet/archive/2010/12/12/1903949.html
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值