以您熟悉的编程语言为例完成一个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; }