以您熟悉的编程语言为例完成一个hello/hi的简单的网络聊天程序---C语言

网络编程通俗意义上说就是使两台联网的计算机互相交换数据。事实上,网络编程要比想象中简单许多。那么两台计算机之间用什么传输数据呢?首先需要物理连接,如今大多数计算机都已经连接到庞大的互联网中,一次我们并不需要担心这一点;而我们真正需要做的就是如何编写传输数据的软件,但实际上,操作系统给我们提供了名为“socket”的部件,socket是网络传输用的软件设备。即使我们对于网络编程不太熟悉,我们也可以通过套接字完成数据传输。

关于进行网络编程,首先要建立套接字socket;socket独立于具体协议的网络编程接口,在TCP/IP模型中,主要位于传输层和应用层之间;

创建socket

对于Linux系统而言,一切皆文件。所以我们一开始的时候就要创建一个文件,也就是我们的socket。有了文件,才能对文件进行各种各样千奇百怪的操作才是嘛,所以我们创建一个socket文件 

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

socket函数会返回一个描述符,也就是一个int整形数字,在Linux下,这个数字就是与系统沟通好的,用来描述当前socket的一个标识;

socket描述

有了这个socket文件之后呢,我们需要告诉操作系统,这个文件的一些特征。socket函数大概就类似于告诉系统,我要创建一个什么类型的文件。而接下来的这段代码,就是要告诉系统,这个有多大,它可以接受怎样的内容

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

我们先将文件给清空,然后再一一告诉系统这个文件相关的东西。

  • AF_INETIPv4 网络协议的套接字类型
  • htonl(INADDR_ANY) 这里告诉系统,这个socket监听所有的网卡,就是指定地址为0.0.0.0的地址
  • htons(atoi(argv[1]))这里告诉系统,监听哪一个端口,该端口由我们指定

其中htons和htons代表了将本地的字符串字节序转化为网络字节序,防止因为本地的大小端存放问题导致出错。

进行绑定

普通的文件创建完了之后,就可以丢那里不管,需要的时候直接写入数据就可以,但毕竟socket不是一般的文件,它需要从网络上来获取数据,所以我们还需要一步绑定的操作,来告诉操作系统,我这个socket需要监听来自哪儿的东西。这样当有数据从指定的地址端口来的时候,系统才知道,要送到这个socket这里。

int bind (int sockfd, struct sockaddr* myaddr, socklen_t addrlen)

通过这个函数,我们就可以绑定到操作系统上,从而接收数据。

开始监听

绑定完了之后,我们需要让socket去监听。bind只是绑定了端口,但是socket并没有对这个端口做什么操作。
所以,我们使用listen来监听我们指定的端口;

int listen(int sockfd, int backlog)

通过这个操作,当有数据传输过来的时候,socket就能够知道,并且能够进行相应的处理。

受理连接

对于一个socket而言,它不一定是给指定的连接使用的,它大部分情况下都是能够接收很多很多的连接,那我们怎么去区分这不同的连接,跟不同的连接做交互呢?所以需要一个函数来负责获取这些连接,将每一个连接打上不同的记号,方便我们进行处理。因此,就有了accept函数;

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

很明显,系统也将不同的连接当成了文件,返回了一个个的文件描述符。因此我们能够根据这不同的文件描述符去区分不同的连接。
注意,当socket没有接收到连接的时候,它会阻塞到这里,直到有连接进来或者出现什么失败为止。

连接服务器

对于客户端的代码而言,基本上都和服务器端的代码相同,但是有个不一样的函数出现在这。对于客户端而言,需要主动的去找服务器,所以客户端的代码需要一个连接的操作;

int connect(int sockfd, struct sockaddr *serv_addr, socklen_t addrlen)

通过connect函数,我们能够连接到想要的服务器,跟其进行通信;

关闭socket

对于任一打开的操作,都要记得关闭操作;

int close(int fd)

这个函数非常简单,传入我们的socket描述符,系统便能够将这个socket
相关的东西断开,方便下次使用。

写入数据

既然有了网络连接,自然是需要传递数据的。我们把socket当成了文件看待,所以像其中写入数据自然是使用write函数

ssize_t write(int fd, const void *buf, size_t nbytes)

这个函数的返回值有点奇怪,它是一种元数据类型(primitive),这里与C语言的语言特点有关,C并未规定int, long short这些数据类型的大小,而且操作系统也在不断的发生变化,由最初的16位发展到现在的64位,所以如果我们直接使用int类型的话,可能在之后的代码中需要进行不断的修改,并且可能在某一系统上,int的大小类型并不符合我们的要求,所以我们利用typedef进行额外的定义,定义出一系列大小规整的类型。

读取数据

读取数据与写入数据很类似,都是很标准的Linux文件操作;

ssize_t read(int fd, void *buf, size_t nbytes)

通过这个函数,我们能够读取到我们需要的大小的数据;

 

整理步骤如下:

第一步:调用socket函数创建套接字;

第二步:调用bind函数分配IP地址以及端口号;

第三步:调用listen函数转换状态为可接受请求状态;

第四步:调用accept函数手里连接请求;

 好了,终于可以写代码了!!!

linux端的服务端的程序如下(C语言):

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

void errorHandling(const char* message);

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

    int sockServ = socket(PF_INET, SOCK_STREAM, 0);////创建套接字,IPV4协议,面向连接的套接字,不存在数据传输方式相同、协议不同的情况

    struct sockaddr_in sockServAddr;
    memset(&sockServAddr, 0, sizeof(sockServAddr));
    //地址族,ipv4
    //字节序须转化为网络序
    sockServAddr.sin_family = AF_INET;
    sockServAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    sockServAddr.sin_port = htons(atoi(argv[1]));////16位TCP/UDP端口号

    //调用bind(),分配IP地址和端口号
    if(bind(sockServ, (struct sockaddr*)& sockServAddr, sizeof(sockServAddr)) ==-1){
        errorHandling("bind() error!");
    }

    //调用listen(),使其可以接受客户端连接
    if(listen(sockServ, 5) == -1){
        errorHandling("listen() error!");
    }

    struct sockaddr_in sockClientAddr;
    socklen_t clientAddrLen = sizeof(sockClientAddr);

    //调用accept(),使其可以接受客户端连接请求
    int sockClient = accept(sockServ, (struct sockaddr*)& sockClientAddr, &clientAddrLen);
    if(sockClient == -1){
        errorHandling("accept() error!");
    }
    else{
        puts("New Client connected...");
    }

    char message[] = "Hello!";
    printf("%s\n",message);

    //向已经连接的客户端传输数据
    write(sockClient, message, strlen(message));

    close(sockClient);
    close(sockServ);

    return 0;
}

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

对于客户端程序,需要一个请求连接函数connect,用来与服务端的listen函数配对;

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

所以客户端的大体流程如下:

第一步:调用socket函数和connect函数;

第二步:与服务端共同运行来收发字符串数据;

下面给出Linux环境下的客户端程序(C):

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

void errorHandling(const char* message);

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

    int sock = socket(PF_INET, SOCK_STREAM, 0);

    struct sockaddr_in sockAddr;
    memset(&sockAddr, 0, sizeof(sockAddr));
    sockAddr.sin_family = AF_INET;
    sockAddr.sin_addr.s_addr = inet_addr(argv[1]);
    sockAddr.sin_port = htons(atoi(argv[2]));

    if(connect(sock, (struct sockaddr*)&sockAddr, sizeof(sockAddr)) == -1){
        errorHandling("connect() error!");
    }

    char buf[32];
    int readLen = read(sock, buf, sizeof(buf)-1);
    if(readLen > 0){
        buf[readLen] = 0;
        printf("Message from server: %s \n", buf);
        printf("I will reply: Hi!\n");
    }
    
    close(sock);

    return 0;
}

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

回顾上面代码我们发现linux环境中的网络编程用到了很多的文件读写操作,所以我又总结了一些linux中的文件操作;

open()函数

功能描述:用于打开或创建文件,在打开或创建文件时可以指定文件的属性及用户的权限等各种参数。

函数原型:int open(const char *pathname,int flags,int perms)

参数:

  • pathname:被打开的文件名(可包括路径名如"dev/ttyS0")
  • flags:文件打开方式,
  • O_RDONLY:以只读方式打开文件
  • O_WRONLY:以只写方式打开文件
  • O_RDWR:以读写方式打开文件
  • O_CREAT:如果改文件不存在,就创建一个新的文件,并用第三个参数为其设置权限
  • O_EXCL:如果使用O_CREAT时文件存在,则返回错误消息。这一参数可测试文件是否存在。此时open是原子操作,防止多个进程同时创建同一个文件
  • O_NOCTTY:使用本参数时,若文件为终端,那么该终端不会成为调用open()的那个进程的控制终端
  • O_TRUNC:若文件已经存在,那么会删除文件中的全部原有数据,并且设置文件大小为0
  • O_APPEND:以添加方式打开文件,在打开文件的同时,文件指针指向文件的末尾,即将写入的数据添加到文件的末尾
  • O_NONBLOCK: 如果pathname指的是一个FIFO、一个块特殊文件或一个字符特殊文件,则此选择项为此文件的本次打开操作和后续的I/O操作设置非阻塞方式。
  • O_SYNC:使每次write都等到物理I/O操作完成。
  • O_RSYNC:read 等待所有写入同一区域的写操作完成后再进行
  • 在open()函数中,falgs参数可以通过“|”组合构成,但前3个标准常量(O_RDONLY,O_WRONLY,和O_RDWR)不能互相组合。
  • perms:被打开文件的存取权限,可以用两种方法表示,可以用一组宏定义:S_I(R/W/X)(USR/GRP/OTH),其中R/W/X表示读写执行权限,
  • USR/GRP/OTH分别表示文件的所有者/文件所属组/其他用户,如S_IRUUR|S_IWUUR|S_IXUUR,(-rex------),也可用八进制800表示同样的权限

返回值:

成功:返回文件描述符

失败:返回-1

 close()函数

功能描述:用于关闭一个被打开的的文件

函数原型:int close(int fd)

参数:fd文件描述符

函数返回值:0成功,-1出错

 read()函数

功能描述: 从文件读取数据。

函数原型:ssize_t read(int fd, void *buf, size_t count);

参数:  

  • fd: 将要读取数据的文件描述词。
  • buf:指缓冲区,即读取的数据会被放到这个缓冲区中去。
  • count: 表示调用一次read操作,应该读多少数量的字符。

返回值:返回所读取的字节数;0(读到EOF);-1(出错)。

write()函数

功能描述: 向文件写入数据。

函数原型:ssize_t write(int fd, void *buf, size_t count);

返回值:写入文件的字节数(成功);-1(出错)

功能:write 函数向 filedes 中写入 count 字节数据,数据来源为 buf 。返回值一般总是等于 count,否则就是出错了。常见的出错原因是磁盘空间满了或者超过了文件大小限制。

对于普通文件,写操作始于 cfo 。如果打开文件时使用了 O_APPEND,则每次写操作都将数据写入文件末尾。成功写入后,cfo 增加,增量为实际写入的字节数。

 lseek()函数

功能描述: 用于在指定的文件描述符中将将文件指针定位到相应位置。

函数原型:off_t lseek(int fd, off_t offset,int whence);

参数:

  • fd;文件描述符
  • offset:偏移量,每一个读写操作所需要移动的距离,单位是字节,可正可负(向前移,向后移)
  • whence:
  • SEEK_SET:当前位置为文件的开头,新位置为偏移量的大小
  • SEEK_CUR:当前位置为指针的位置,新位置为当前位置加上偏移量
  • SEEK_END:当前位置为文件的结尾,新位置为文件大小加上偏移量的大小

返回值:

成功:返回当前位移

失败:返回-1

给出运行结果:

 

 

使用GDB追踪系统调用:

 

 然后查看内核源码;

/*
 *    System call vectors.
 *
 *    Argument checking cleaned up. Saved 20% in size.
 * This function doesn't need to set the kernel lock because
 * it is set by the callees.
 */

asmlinkage long sys_socketcall(int call, unsigned long __user *args)
{
    unsigned long a[6];
    unsigned long a0, a1;
    int err;

    if (call < 1 || call > SYS_RECVMSG)
        return -EINVAL;

    /* copy_from_user should be SMP safe. */
    if (copy_from_user(a, args, nargs[call]))
        return -EFAULT;

    err = audit_socketcall(nargs[call] / sizeof(unsigned long), a);
    if (err)
        return err;

    a0 = a[0];
    a1 = a[1];

    switch (call) {
    case SYS_SOCKET:
        err = sys_socket(a0, a1, a[2]);
        break;
    case SYS_BIND:
        err = sys_bind(a0, (struct sockaddr __user *)a1, a[2]);
        break;
    case SYS_CONNECT:
        err = sys_connect(a0, (struct sockaddr __user *)a1, a[2]);
        break;
    case SYS_LISTEN:
        err = sys_listen(a0, a1);
        break;
    case SYS_ACCEPT:
        err =
         sys_accept(a0, (struct sockaddr __user *)a1,
             (int __user *)a[2]);
        break;
    case SYS_GETSOCKNAME:
        err =
         sys_getsockname(a0, (struct sockaddr __user *)a1,
                 (int __user *)a[2]);
        break;
    case SYS_GETPEERNAME:
        err =
         sys_getpeername(a0, (struct sockaddr __user *)a1,
                 (int __user *)a[2]);
        break;
    case SYS_SOCKETPAIR:
        err = sys_socketpair(a0, a1, a[2], (int __user *)a[3]);
        break;
    case SYS_SEND:
        err = sys_send(a0, (void __user *)a1, a[2], a[3]);
        break;
    case SYS_SENDTO:
        err = sys_sendto(a0, (void __user *)a1, a[2], a[3],
                 (struct sockaddr __user *)a[4], a[5]);
        break;
    case SYS_RECV:
        err = sys_recv(a0, (void __user *)a1, a[2], a[3]);
        break;
    case SYS_RECVFROM:
        err = sys_recvfrom(a0, (void __user *)a1, a[2], a[3],
                 (struct sockaddr __user *)a[4],
                 (int __user *)a[5]);
        break;
    case SYS_SHUTDOWN:
        err = sys_shutdown(a0, a1);
        break;
    case SYS_SETSOCKOPT:
        err = sys_setsockopt(a0, a1, a[2], (char __user *)a[3], a[4]);
        break;
    case SYS_GETSOCKOPT:
        err =
         sys_getsockopt(a0, a1, a[2], (char __user *)a[3],
                 (int __user *)a[4]);
        break;
    case SYS_SENDMSG:
        err = sys_sendmsg(a0, (struct msghdr __user *)a1, a[2]);
        break;
    case SYS_RECVMSG:
        err = sys_recvmsg(a0, (struct msghdr __user *)a1, a[2]);
        break;
    default:
        err = -EINVAL;
        break;
    }
    return err;
}
asmlinkage long sys_socket(int family, int type, int protocol)
{
    int retval;
    struct socket *sock;

    retval = sock_create(family, type, protocol, &sock);
    if (retval < 0)
        goto out;

    retval = sock_map_fd(sock);
    if (retval < 0)
        goto out_release;

out:
    /* It may be already another descriptor 8) Not kernel problem. */
    return retval;

out_release:
    sock_release(sock);
    return retval;
}

 

posted @ 2019-11-26 20:29  phil_cao  阅读(594)  评论(0编辑  收藏  举报