网络编程和套接字编程
网络编程又称为套接字编程,就是编写一段程序,使得两台连网的计算机彼此之间能够交换数据。那么,这两台计算机用什么传输数据呢?首先,须要物理链接,将一台台独立的计算机经过物理线路链接在一块儿,造成一个网络,使得数据能够经过物理介质进行传输,这一点咱们不用过多去关注。咱们只须要考虑,如何编写数据传输的程序。编写数据传输的数据,其实说难也难说简单也简单,说它简单,是由于操做系统为咱们提供了“套接字”(socket)的部件,咱们能够经过套接字完成数据在网络中的传输,实现两台计算机互相交换数据,即使咱们并不懂socket的原理,这是它的简单之处。可是说它难,是如何使用socket编写高性能的数据传输程序,这是它的难点数组
那么,为何要用“套接字”这个词呢?“套接字”包含链接的含义,就像咱们把插头插到插座上就能得到电力供给,一样,为了与远程计算机进行数据传输,就须要链接到因特网,而编程中的“套接字”就是用来链接网络的工具 bash
既然是传输数据,那么就有发送方和接收方,对于套接字来讲,发送和接收是有区别的,咱们先来讨论用于接收的套接字的建立过程服务器
1.调用socket函数网络
#include <sys/socket.h> int socket(int domain, int type, int protocol);
建立套接字,成功时返回文件描述符,失败时返回-1dom
2.调用bind函数socket
#include <sys/socket.h> int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);
调用bind函数给套接字分配IP地址和端口,成功时返回0,失败时返回-1tcp
3.调用listen函数函数
#include <sys/socket.h> int listen(int sockfd, int backlog);
将套接字转化为可接收链接的状态,成功时返回0,失败时返回-1工具
4.调用accept函数
#include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
等待数据传输,成功时返回文件描述符,失败时返回-1
网络编程中接收链接请求的套接字建立过程以下:
编写服务端/客户端套接字
服务器端(server)是可以受理链接请求的程序,下面构建服务端程序以验证以前提到的函数调用过程,该服务端程序收到链接请求后向请求者返回“Hello world!”答复
hello_server.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> void error_handling(char *message); int main(int argc, char *argv[]) { int serv_sock; int clnt_sock; struct sockaddr_in serv_addr; struct sockaddr_in clnt_addr; socklen_t clnt_addr_size; char message[] = "Hello world!"; if (argc != 2) { printf("Usage: %s <port>\n", argv[0]); exit(1); } serv_sock = socket(AF_INET, SOCK_STREAM, 0); //<1> if (serv_sock == -1) error_handling("sock() error"); 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])); if (bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) //<2> error_handling("bind() error"); if (listen(serv_sock, 5) == -1) //<3> error_handling("listen() error"); clnt_addr_size = sizeof(clnt_addr); clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size); //<4> if (clnt_sock == -1) error_handling("accept() error"); write(clnt_sock, message, sizeof(message)); //<5> close(clnt_sock); close(serv_sock); return 0; } void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); }
编译并运行hello_server.c,建立等待链接请求的服务器端
# gcc hello_server.c -o hello_server # ls hello_server hello_server.c # ./hello_server 8500
服务器端建立的套接字又称为服务器端套接字或监听套接字,接下来介绍的套接字是用于请求链接服务器端的客户端套接字
#include <sys/socket.h> int connect(int sock_fd, struct sockaddr *serv_addr, socklen_t addrlen);
connect为客户端用于请求链接的函数,成功时返回0,失败时返回-1
客户端只有“调用socket函数建立套接字”和“调用connect函数向服务器端发送链接请求”这两个步骤。所以,比服务器端简单
hello_client.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> void error_handling(char *message); int main(int argc, char *argv[]) { int sock; struct sockaddr_in serv_addr; char message[30]; int str_len; if (argc != 3) { printf("Usage: %s <IP> <port>\n", argv[0]); exit(1); } sock = socket(AF_INET, SOCK_STREAM, 0); //<1> if (sock == -1) error_handling("sock() error"); memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = inet_addr(argv[1]); serv_addr.sin_port = htons(atoi(argv[2])); if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) //<2> error_handling("connect() error!"); str_len = read(sock, message, sizeof(message) - 1); if (str_len == -1) error_handling("read() error!"); printf("Message from server: %s\n", message); close(sock); return 0; } void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); }
编译并运行hello_client.c
# gcc hello_client.c -o hello_client # ls hello_client hello_client.c hello_server hello_server.c # ./hello_client 127.0.0.1 8500 Message from server: Hello world!
基于Linux的文件操做
对于Linux而言,socket操做与文件操做没有区别。在Linux世界里,socket也被认为是文件的一种,所以在网络数据传输过程当中天然可使用文件I/O的相关函数,若是想使用Linux提供的文件I/O函数,首先应该理解好文件描述符的概念
文件描述符 | 对象 |
0 | 标准输入:Standard Input |
1 | 标准输出:Standard Output |
2 | 标准错误:Standard Error |
文件和套接字通常通过建立过程才会被分配文件描述符,表1-1中的3种输入输出对象即便未通过特殊的建立过程,程序开始运行后也会被自动分配文件描述符,后面还会讲解其使用方法和含义
打开文件
首先介绍打开文件以读写数据的函数。调用此函数时需传递两个参数:第一个参数是打开的目标文件名及路径信息,第二个参数是文件打开模式(文件特性信息)
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int open(const char *path, int flag);//成功时返回文件描述符,失败时返回-1
打开模式 | 含义 |
O_CREAT | 必要时建立文件 |
O_TRUNC | 删除所有现有数据 |
O_APPEND | 维持现有数据,并将新数据追加到其后 |
O_RDONLY | 只读打开 |
O_WRONLY | 只写打开 |
O_RDWR | 读写打开 |
若是flag须要传递多个参数,则经过位或运算(OR)符组合并传递
关闭文件
文件有打开,天然也有关闭。文件是系统中的一种资源,若是咱们只打开文件,却不关闭,其余程序就不能使用此资源。且文件打开后,可能有一些操做被缓冲在内存中,若不正常关闭,缓冲在内存中的数据就不能真正写入到文件中,可能形成数据丢失,下面介绍关闭文件时调用的函数
#include <unistd.h> int close(int fd);//成功时返回文件描述符,失败时返回-1
若调用此函数的同时传递文件描述符参数,则关闭相应文件。另外须要注意的是,此函数不只能够关闭文件,还能够关闭套接字。这再次证实Linux操做系统不区分文件与套接字的特色
将数据写入文件
接下来介绍的write函数用于向文件输出数据。固然,Linux中不区分文件与套接字,所以,经过套接字向其余计算机传输数据时也会调用到该函数
include <unistd.h> ssize_t write(int fd, const void *buf, size_t nbytes);//成功时返回写入的字节数,失败时返回-1
此函数定义中,size_t是经过typedef声明的unsigned int类型。对ssize_t来讲,size_t前面多加一个s表明signed,即ssize_t是经过typedef声明的signed int类型
咱们来看下面一段程序, low_open.c将完成建立新文件并保存数据
low_open.c
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> void error_handling(char *message); int main(void) { int fd; char buf[] = "Let's go!\n"; fd = open("data.txt", O_CREAT | O_WRONLY | O_TRUNC); //<1> if (fd == -1) error_handling("open() error"); printf("file descriptor:%d\n", fd); if (write(fd, buf, sizeof(buf)) == -1) //<2> error_handling("write() error!"); close(fd); return 0; } void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); }
编译并运行low_open.c
# gcc low_open.c -o low_open # ls low_open low_open.c # ./low_open file descriptor:3 # ls data.txt low_open low_open.c # cat data.txt Let's go!
读取文件中的数据
与以前的write函数相对应,read函数是用来接收数据的
include <unistd.h> ssize_t read(int fd, void *buf, size_t nbytes);//成功时返回接收的字节数(但遇到文件结尾则返回0),失败时返回-1
下面,咱们经过low_read.c来读取data.txt中保存的数据
low_read.c
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #define BUF_SIZE 100 void error_handling(char *message); int main(void) { int fd; char buf[BUF_SIZE]; fd = open("data.txt", O_RDONLY); //<1> if (fd == -1) error_handling("open()error"); printf("file descriptor:%d\n", fd); if (read(fd, buf, sizeof(buf)) == -1) //<2> error_handling("read()error"); printf("file data:%s", buf); close(fd); return 0; } void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); }
编译并运行low_read.c
# gcc low_read.c -o low_read [root@bogon tcp_ip]# ls data.txt low_open low_open.c low_read low_read.c [root@bogon tcp_ip]# ./low_read file descriptor:3 file data:Let's go!
文件描述符与套接字
下面将同时建立文件和套接字,并用整数形态比较返回的文件描述符值
fd_seri.c
#include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <sys/socket.h> int main(void) { int fd1, fd2, fd3; fd1 = socket(PF_INET, SOCK_STREAM, 0); fd2 = open("test.dat", O_CREAT | O_WRONLY | O_TRUNC); fd3 = socket(PF_INET, SOCK_DGRAM, 0); printf("file descriptor 1:%d\n", fd1); printf("file descriptor 2:%d\n", fd2); printf("file descriptor 3:%d\n", fd3); close(fd1); close(fd2); close(fd3); return 0; }
编译并运行fd_seri.c
# gcc fd_seri.c -o fd_seri # ./fd_seri file descriptor 1:3 file descriptor 2:4 file descriptor 3:5
从输出的文件描述符整数值能够看出,描述符从3开始由小到大的顺序编号,由于0、一、2是分配给标准I/O的描述符