Linux网络之socket编程 (1)

在谈到socket编程之前,首先我们要知道一点预备知识。


预备知识:

1、网路字节序全部采用大端字节序。

关于字节序的详解,戳链接 查看,这里不做解释。

2、在编程之前,我们有必要了解,什么是socket?

socket,又叫做套接字。我们都应该知道,在网络中,IP地址+ 端口号,可以唯一表示互联网中的一个进程,因此,我们将  IP地址+端口号 称为socket。

socket API是一套抽象的网络编程接口,适用于各种底层网络协议,包括IPv4,IPv6以及UNIX Domain Socket等,但各种网络协议地址格式并不相同,举两个的例子,IPv4和Unix Domain Socket,如图:

现在网络协议中最常用的依旧是IPv4,本文以IPv4为重点进行介绍。

可以发现,IPv4的地址结构大小为16字节,末尾填充了8字节的其他内容,这个我们不关心,需要我们注意的是上面三段。

a、前16位表示的是地址类型,可以注意到,其他类型的地址结构也有,这是用来区分不同协议类型的部分;

b、16位端口号,指明协议使用的端口;

c、32位IP地址,指明通信时使用的IP地址。

对于不同的协议,地址结构很明显是不同的,举个例子,IPv6的IP地址长度和Ipv6的IP地址长度很明显不同。互联网中有众多的协议,难道要针对每种协议提供一套接口?

当然,Linux不会这么干,Linux提供了一套抽象出来的标准接口,叫做struct sockaddr。对于不同的网络协议的地址格式,有一个共同点,就是前16位用来表示地址类型【注1】。那么对待不同的网络协议,可以各自定义自己的地址类型,在使用的过程中,只需要强制类型转化为标准格式即可。区分不同的地址协议,仅仅需要struct sockaddr的前16位足以。

基于POSIX规范,对于IPv4协议,我们只需要关注整个地址结构中的3个字段sin_family、sin_addr、sin_port(分别表示地址类型,IP地址,port端口号,具体信息后面说)。

【注1】:IPv4的地址类型为 AF_INET;IPv6的地址类型为AF_INET6;UNIX Domain Socket的地址类型为AF_UNIX。

socket通信:

在TCP协议中,当我们使用socket通信时,通信双方(连接的两个进程)都需要有一套自己的socket来标识,那么这两个socket组成的socket pair就唯一标识了这一组连接。

我们建立的通信是在应用层之上的,TCP/IP协议设计的应用层编程接口又叫做socket API ,本文的重点是如何利用这些API来实现两个进程之间通过网络通信。

首先了解一下关于socket通信的整个流程。如下图。

对上图首先有一个大致印象即可,下面我们来看TCP协议提供的这一套API。

首先讨论server端:

1、创建 socket

       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>
       int socket(int domain, int type, int protocol);
               #  creates  an  endpoint  for  communication  and  returns a descriptor
               # domain:地址类型,上面解释过
               # type:流式套接,SOCK_STREAM代表TCP,SOCK_DGRAM代表UDP
               # 一般设置为0
               # 返回值为文件描述符,默认从3开始。失败返回-1

2、绑定套接字 bind 

       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>
       int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
               # 参数1 --> 创建socket时获得的文件描述符
               # 参数2 --> 指向特定协议的地址结构的指针
               # 参数3 --> 该地址结构的长度       sockaddr_in的长度,即网络地址的长度
               # 成功返回0, 失败返回-1

在struct sockaddr 结构中,可以指定IP或者端口号(下面说具体如何做),当然可以任意指定其中一个,或者都不指定。

端口:如果 server 没有使用bind绑定端口号的话,内核会为该 socket 选择一个临时端口,这个端口的选择往往是随机的。但是对于TCP服务器而言,它的端口应该是被众所周知(well_known)的。如果端口都是随机的话,那么 client 就不能保证可以正常访问到 server 。当然对于client而言是,让内核来选择端口是一件很正常的事。

IP地址:对于服务器而言,如果绑定了IP地址,这就限定了该socket只接收目的地为这个IP地址的client连接;如果TCP服务器没有将IP地址捆绑到该 socket 上,内核就把客户发送的SYN(TCP/IP建立连接时的握手信号)作为服务器的源IP地址。

当我们希望内核自动分配一个端口地址的话,必须注意的是,bind函数本身并不返回所选择的端口号,为了得到内核选择的端口号,必须调用  getsockname  函数。

接下来是IPv4的地址空间结构,

struct sockaddr_in {
        sa_family_t           sin_family;    
        __be16                sin_port;      
        struct in_addr        sin_addr;       
        unsigned char         __pad[__SOCK_SIZE__ - sizeof(short int) -  sizeof(unsigned short int) - sizeof(struct i    n_addr)];
};
               # 参数1 --> 地址类型
               # 参数2 --> 端口号
               # 参数3 --> IP地址,结构体定义如下
               # 参数4 --> 填充位 PAD ,不需要时不用关心该成员
 struct in_addr {
         __be32  s_addr;
 };

如果想由内核选择端口,则sin_port值为0;

如果想由内核选择IP,则sin_addr.s_addr = htonl(INADDR_ANY);

这里的 IP地址 和 端口号由于存在大小端的问题,因此需要进行转换,转换函数如下:

端口转换函数

 #include <arpa/inet.h>
       uint32_t htonl(uint32_t hostlong);
       uint16_t htons(uint16_t hostshort);
       uint32_t ntohl(uint32_t netlong);
       uint16_t ntohs(uint16_t netshort);

IP 地址转换函数

  #include <sys/socket.h>
  #include <netinet/in.h>
  #include <arpa/inet.h>
       in_addr_t inet_addr(const char *cp);

3、监听 listen


监听函数仅由TCP服务器来调用,主要做两件事:

a、调用 listen 函数,使得套接字从 CLOSED 转换到 LISTEN状态,将一个未连接的主动套接字转换为被动套接字,指示内核应该接受指向该套接字的连接请求;

b、该函数的第二个参数指定了内核应该为相应套接字排队的最大连接个数。

    #include <sys/types.h>
    #include <sys/socket.h>
       int listen(int sockfd, int backlog);
               # 参数1 --> 创建socket时获得的文件描述符
               # 参数2 --> 一般设置为5
               # 成返回0, 失败返回-1

该函数通常在调用socket()、bind()函数之后,accept()函数之前调用。

   理解backlog参数:

内核在为任何一个给定的监听套接字维护两个队列:

a、未完成连接队列。由client发起连接并且已经完成第一次握手,但尚未完成三次握手,这些套接字处于SYN_RCVD状态;

b、已完成连接队列。已经完成三次握手的客户端对应一项,这些套接字呼吁ESTABLISHED状态。

注意:永远不要将backlog设置为0,即使不想任何客户连接到你的监听套接字上。

4、 accept

该函数有TCP服务器调用,用于从已完成的连接队列头返回下一个已完成连接。如果已完成连接列队为空,那么进程进入睡眠状态。套接字默认为阻塞状态。

    #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>
       int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
               # 参数1 --> 监听套接字;创建socket时获得的文件描述符
               # 参数2、3为输出型参数,用来获取连接client端的协议地址
               # socket_t len = sizeof(struct socketaddr_in);一变量,两应用,既做输入,又做输出
               # 成功返回一个全新的文件描述符,我们把它叫做连接套接字。失败后-1。返回值是真正实现数据通信的套接字

     需要注意的是,server通过socket建立的自己的套接字,这个套接字是well_known的,只是用来建立连接的,并不是真正数据传输使用的。真正的数据传输是建立在client的套接字上的,server通过accept的返回值获取client的套接字

由于套接字也是文件,读写可以使用read 和 write 函数,使用完毕,需要close 文件。

这就需要重提一个概念, Linux一切皆文件,只不过是相同的接口,不同的底层实现。

client端:

1、socket

用法同server


2、连接 connect

       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>
       int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
               # client 用来发起连接请求(三次握手)
               # 参数1 --> client的套接字描述符
               # 参数2 --> 指向套接字地址结构的指针,需要手动来初始化
               # 参数3 --> 地址结构的大小

关于server 和client的读写操作,可以直接使用write和read 函数,这里不再细说,接下来给出测试用例,

//server.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
void Usage(char *msg)
{
    printf("invalid Input!\n");
    printf("Usage: %s [ip] [port]\n",msg);
}
int create_socket(char *port, char *addr)
{
    // 1.create an endpoint for communication
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if(sock < 0)
    {
        perror("socket error");
        exit(1);
    }
    //
    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_port   = htons(atoi(port));
    local.sin_addr.s_addr   = inet_addr(addr);
    if(bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
    {
        perror("bing error");
        close(sock);
        exit(2);
    }
    if(listen(sock, 5) < 0)
    {
        perror("listen error");
        close(sock);
        exit(3);
    }
    return sock;
}
int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        return 1;
    }
    // create socket
    int listen_sock = create_socket(argv[2], argv[1]);
    struct sockaddr_in client;
    socklen_t len = sizeof(struct sockaddr);
    while(1){
        char buf[1024];
        memset(buf, 0, sizeof(buf));
        int ret = 0;
        if((ret = accept(listen_sock, (struct sockaddr*)&client, &len)) < 0)
        {
            perror("accept error");
            continue;
        }
        while(1)
        {
            ssize_t _s = read(ret, buf, sizeof(buf)-1);
            printf("*************************\n");
            if(_s > 0)
            {
                printf("client# ");
                fflush(stdout);
                buf[_s-1] = 0;
                printf("%s\n", buf);
            }
            else if (_s == 0)
            {
                printf("client quit!\n");
                break;
            }
        }
    }
    return 0;
}
/***************************************************************************/
// client.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[])
{
    // check Input
    if(argc != 3)
    {
        printf("Invalid Input!\n");
        printf("Usage# %s [ip] [port]\n");
        return 1;
    }
    // create socket
    int client_sock = socket(AF_INET, SOCK_STREAM, 0);
    if(client_sock < 0)
    {
        perror("socket error");
        exit(1);   
    }
    // connect
    struct sockaddr_in server_addr;
    server_addr.sin_family      = AF_INET;
    server_addr.sin_port        = htons(atoi(argv[2]));
    server_addr.sin_addr.s_addr = inet_addr(argv[1]);
    int ret = connect(client_sock, (struct sockaddr *)&server_addr, sizeof(struct sockaddr_in));
    if(ret < 0)
    {
        perror("connect error");
        printf("*****************8");
        close(client_sock);
        return 4;
    }
    printf("connect success!\n");
    // send
    while(1)
    {
        printf("Send# ");
        fflush(stdout);
        char buf[1024];
        memset(buf, 0, sizeof(buf));
        ssize_t _s  = read(0, buf, sizeof(buf));
        printf("*************************\n");
        if(_s < 0)
        {
            perror("read error");
            close(client_sock);
            return 5;
        }
        write(client_sock ,buf, _s);
        memset(buf, 0, sizeof(buf));
    }
    return 0;
}

在不同的终端下,分别运行server和client,IP地址选择server端的IP,端口号自定义,然后可以看到下面的打印结果:

这里可以实现的网络通信,是建立在局域网范围内的,当然如果找不下两台主机的话,可以使用127.0.0.1的IP地址,做本地环回测试,也就是说server和client的端口都放在了一台主机上。这里自定义的端口号,最好选用大于1024的端口,防止造成端口冲突。

上面的代码只是用来测试使用的,真正服务器上写出这样的代码是要出大麻烦的,之后,我们会谈到一些特殊的情况,关于上面的测试代码,这里给出链接,可以自己下载使用:

https://github.com/muhuizz/Linux/tree/master/Linux%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B/code/socket-1

-----muhuizz整理

时间: 2024-10-18 15:35:58

Linux网络之socket编程 (1)的相关文章

Linux下的socket编程

网络通信编程即编写通过计算机与其他程序之间进行通讯的程序,相互通信的程序中一方可以称为客户端程序,另一方称为服务程序,应用系统提供Socket编程接口可以编写自己的网络程序. 一  通过TCP/IP协议进行传输 TCP:为应用程序提供可靠的通信连接.适合一次传输大批的数据情况.并使用于要求得到的响应程序. UDP:提供无线连接通信,且对传送包进行可靠性保证.适合一次传输少量的数据,可靠性则由应用层来负责. 二  Socket套接字 网络通信编程通过socket接口来进行的.socket接口是TC

Linux下的socket编程实践(一) 网络基本知识以及 TCP/IP简述

ISO/OSI七层参考模型 1.物理层:主要定义物理设备标准,如网线的接口类型.光纤的接口类型.各种传输介质的传输速率等.它的主要作用是传输比特流(就是由1.0转化为电流强弱来进行传输,到达目的地后再转化为1.0,也就是我们常说的数模转换与模数转换).这一层的数据叫做比特.(标志:RJ-45) 2.数据链路层:定义了如何让格式化数据以进行传输,以及如何让控制对物理介质的访问.这一层通常还提供错误检测和纠正,以确保数据的可靠传输,交换机属于本层. 3.网络层:在位于不同地理位置的网络中的两个主机系

网络编程学习笔记:linux下的socket编程

socket是进程通信的一种方式,通过调用一些API可以实现进程间通信,建立连接以及收发信息的过程如下图所示: 这些函数的用法如下: 1.int socket(int protocolFamily, int type, int protocol); 返回描述符sockfd l  protocolFamily:协议族,AF_INET(IPV4).AF_INET6(IPV6).AF_LOCAL(或称AF_UNIX,unix域socket).AF_ROUTE等.协议族决定了socket的地址类型,在通

LINUX 下 ipv6 socket 编程

大家都知道,随着互联网上主机数量的增多,现有的32位IP地址已经不够用了,所以推出了下一代IP地址IPv6,写网络程序的要稍微改变一下现有的网络程序适应IPv6网络是相当容易的事.对于我们来说就是IP地址变化了,所以程序里在用到IP地址的地方做相应的改变就可以了. 记住:主要是改变程序里设置IP地址和端口等部分的代码. 服务器端源代码如下:/***********************/#include <stdio.h>#include <stdlib.h>#include &

Linux下的socket编程实践(八) Select的限制和poll(并发的初步知识)

select的限制 用select实现的并发服务器,能达到的并发数一般受两方面限制: 1)一个进程能打开的最大文件描述符限制.这可以通过调整内核参数来改变.可以通过ulimit -n(number)来调整或者使用setrlimit函数设置(需要root权限),但一个系统所能打开的最大数也是有限的,跟内存大小有关,可以通过cat /proc/sys/fs/file-max 查看. 2)select中的fd_set集合容量的限制(FD_SETSIZE,一般为1024),这需要重新编译内核才能改变.

Linux下的socket编程实践(十) 基本UDP编程细节

在我的这两篇博客中,简单介绍并实现了基于UDP(TCP)的windows(UNIX下流程基本一致)下的服务端和客户端的程序,本文继续探讨关于UDP编程的一些细节. http://blog.csdn.net/nk_test/article/details/47733307 http://blog.csdn.net/nk_test/article/details/47756381 下图是一个简单的UDP客户/服务器模型: 我在这里也实现了一个简单的UDP回射服务器/客户端: /**实践: 实现一个基

Linux下的socket编程实践(四)TCP的粘包问题和常用解决方案

TCP粘包问题的产生 由于TCP协议是基于字节流并且无边界的传输协议, 因此很有可能产生粘包问题.此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段.若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,但是接收方并不知道要一次接收多少字节的数据,这样接收方就收到了粘包数据.具体可以见下图: 假设主机A send了两条消息M1和M2 各10k 给主机B,由于主机B一次提取的字节数

iOS开发网络篇—Socket编程

转自http://www.mamicode.com/info-detail-877996.html 一.网络各个协议:TCP/IP.SOCKET.HTTP等 网络七层由下往上分别为物理层.数据链路层.网络层.传输层.会话层.表示层和应用层. 其中物理层.数据链路层和网络层通常被称作媒体层,是网络工程师所研究的对象: 传输层.会话层.表示层和应用层则被称作主机层,是用户所面向和关心的内容. http协议对应于应用层 tcp协议对应于传输层 ip协议对应于网络层 三者本质上没有可比性.  何况HTT

Linux - 非阻塞socket编程处理EAGAIN错误

在linux进行非阻塞的socket接收数据时经常出现Resource temporarily unavailable,errno代码为11(EAGAIN),这表明你在非阻塞模式下调用了阻塞操作,在该操作没有完成就返回这个错误,这个错误不会破坏socket的同步,不用管它,下次循环接着recv就可以. 对非阻塞socket而言,EAGAIN不是一种错误.在VxWorks和Windows上,EAGAIN的名字叫做EWOULDBLOCK. 另外,如果出现EINTR即errno为4,错误描述Inter