网络套接字socket编程之TCP

xiaoxiao2021-02-28  127

概述

在刚开始学习网络套接字编程时,接触到了socket这个词。我们在以前学到Linux的时候了解到Linux下一切皆文件,并大致分为普通文件、目录文件、连接文件、设备和设备文件、管道。而套接字呢是用来实现网上的进程间的通信的,所以套接字也是文件。在TCP/IP协议中,IP地址和端口号唯一标识网络中的一个唯一进程,IP地址和端口号就是套接字。

网络字节序

网络中要实现通信,少不了数据的传输。所以这里就引入了网络字节序的概念。 在前边的学习中,我们接触到大端和小端的概念。小端:数据的地位在低地址,高位在高地址;大端:数据的低位在高地址,高位在低地址。网络数据流同样也有大端和小端之分。网络数据流先发出的是低地址,后发出的是高地址。TCP/IP规定,网络数据流采用大端字节序,即就是低位在高地址。我们之所以会说到大端和小端?是因为,网络通信的时候必须知道端口号,如果发送端是大端字节序,接收端是小端字节序,那么最后看到的端口号就是不正确的端口号,所以,我们必须将端口号在发送端和接收端之间转换成统一的字节序形式。

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。

这些函数名很好记,h表示host(主机),n表示network(网络),l表示32位长整数(即long类型),s表示16位短整数(即short类型)。例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。

socket地址的数据结构类型

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、 IPv6。然而,各种网络协议的地址格式并不相同,如下图示:

IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表中,包括16位端口号和32位IP地址。

socket编程的相关函数

套接字创建函数socket

这个函数就是用来创建套接字的。

其中,domain是表示建立的socket类型,参数如下:

至于type则是表示的是数据报传输还是字节流传输,参数如下:

最后呢protocol则表示创建的方式,一般缺省为0。

服务器绑定函数bind

这个函数用来绑定服务器的ip地址与端口号。

其中,参数sockfd表示的是服务器的套接字——也就是socket函数的返回结果。

而参数addr表示的是socket服务器的地址内容,结构体内的变量则去填写ip地址与端口号。

而sockaddr_in这个结构体成员则是如下所示:

这里呢我需要介绍一些对IP地址进行操作的函数:

其中呢我主要介绍一下inet_addr和inet_ntoa这两个,前者是将字符串表示的ip地址转成点分十进制表示的ip地址,而后者则是将一个点分十进制的ip地址转成一个字符串。

至于最后的addrlen则表示的是传入的addr的长度,可用sizeof得到。

设置监听状态函数listen

这个函数是用来设置sockfd套接字为监听状态的,用来监听客户端的连接。

其中sockfd表示要被设置的套接字,而backlog表示的是服务器链接达到最大的数量之后,还可以放到等待队列的链接个数,所以一般不要设太大。

请求连接函数connect

一般呢这个函数用于客户端,用来请求对服务器的连接。

其中呢参数sockfd表示的表示的是要链接到服务器的客户端套接字;参数addr表示的是服务器的地址与端口号;参数addrlen表示的是addr的大小一般使用sizeof得到。

单进程的套接字TCP通信

服务器:

#include<stdio.h> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #include<arpa/inet.h> #include<string.h> #include<stdlib.h> static void usage(const char* proc) { printf("Usage: %s [local_ip] [local_port]\n", proc); } int startup(const char* _ip, int _port)//建立监听套接字 { int sock = socket(AF_INET, SOCK_STREAM, 0);//创建一个socket if(sock < 0){ perror("socket"); exit(2); } //为网络协议地址赋值 struct sockaddr_in local; local.sin_family = AF_INET; local.sin_port = htons(_port); local.sin_addr.s_addr = inet_addr(_ip); if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0){//绑定ip与端口到套接字上面 perror("bind"); exit(3); } if(listen(sock, 5)){//设置监听套接字,5表示监听的最大个数 perror("listen"); exit(4); } return sock; } int main(int argc, char* argv[]) { if(argc != 3){ usage(argv[0]); exit(1); } int server_sock = startup(argv[1],atoi(argv[2])); while(1){ struct sockaddr_in client_addr; socklen_t len = sizeof(client_addr); int client_sock = accept(server_sock, (struct sockaddr*)&client_addr, &len);//获取连接并保存连接到的客户端地址 if(client_sock < 0){ perror("accept"); continue; } printf("IP is %s, Port is %d\n", inet_ntoa(client_addr.sin_addr), htons(client_addr.sin_port));//打印连接到的ip地址和端口号 char buf[1024]; while(1){ int ret = read(client_sock, buf, sizeof(buf)-1);//服务器是先读后写,fd表示的是收到的客户端文件描述符 if(ret < 0){ perror("read"); exit(5); }else if(ret == 0){ printf("client quit\n"); close(client_sock); break; }else{ buf[ret] = 0; printf("client #:%s\n", buf); write(client_sock, buf, strlen(buf)); //读完之后将读到的数据再次返回写给客户端   } } close(client_sock); } close(server_sock); return 0; } 客户端:

#include<stdio.h> #include<sys/types.h> #include<sys/socket.h> #include<string.h> #include<stdlib.h> #include<netinet/in.h> void usage(const char* proc) { printf("Usage: %s [local_ip] [local_port]\n", proc); } int main(int argc, char* argv[]) { if(argc != 3){ usage(argv[0]); exit(1); } //客户端不用绑定ip与端口号,因为是多对一的 int sock = socket(AF_INET, SOCK_STREAM, 0); if(sock < 0){ perror("socket"); exit(2); } struct sockaddr_in local; local.sin_family = AF_INET; local.sin_port = htons(atoi(argv[2])); local.sin_addr.s_addr = inet_addr(argv[1]); if(connect(sock, (struct sockaddr*)&local, sizeof(local)) < 0){用connect函数请求连接到服务器 perror("connect"); exit(3); } printf("connect success!\n"); char buf[1024]; while(1){ printf("client #:");//开始通信,客户端是先写后读 fflush(stdout); int ret = read(0, buf, sizeof(buf)-1);//从标准输入中读取数据 if(ret <= 0){ perror("read"); exit(4); }else{ buf[ret-1] = 0; write(sock, buf, strlen(buf)); } ret = read(sock, buf, sizeof(buf)-1);//再从套接字中读取数据 if(ret < 0){ perror("read"); exit(5); }else if(ret == 0){ printf("server quit\n"); break; }else{ buf[ret] = 0; printf("server #:%s\n", buf); } } close(sock); return 0; } 我们在实现了上面的代码后运行一下:

如果不加上ip地址和端口号,就会报错

那么现在加上端口号和ip地址去运行:

先运行tcp_server,然后等待tcp_client的运行

运行tcp_client,然后就会看到连接成功

此时服务器端就会打印出客户端的ip地址和端口号。

然后客户端输入nihao,那么服务器端也会显示nihao。

多进程的套接字TCP通信

在上面呢我们实现了一个单进程TCP通信,但是呢现实生活中不可能就一个进程进行通信的,而在这之前我们也提到过多进程的概念以及多进程的编写,所以呢在这里我们也实现一下多进程TCP通信。

服务器端:

#include<stdio.h> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #include<arpa/inet.h> #include<string.h> #include<stdlib.h> static void usage(const char* proc) { printf("Usage: %s [local_ip] [local_port]\n", proc); } int startup(const char* _ip, int _port) { int sock = socket(AF_INET, SOCK_STREAM, 0); if(sock < 0){ perror("socket"); exit(2); } //为网络协议地址赋值 struct sockaddr_in local; local.sin_family = AF_INET; local.sin_port = htons(_port);//htons就是将host主机转换为net网络中的,而s代表short,l则代表long local.sin_addr.s_addr = inet_addr(_ip); if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0){ perror("bind"); exit(3); } if(listen(sock, 5)){//5表示监听的最大个数 perror("listen"); exit(4); } return sock; } int main(int argc, char* argv[]) { if(argc != 3){ usage(argv[0]); exit(1); } int server_sock = startup(argv[1],atoi(argv[2])); while(1){ struct sockaddr_in client_addr; socklen_t len = sizeof(client_addr); int client_sock = accept(server_sock, (struct sockaddr*)&client_addr, &len); if(client_sock < 0){ perror("accept"); continue; } printf("IP is %s, Port is %d\n", inet_ntoa(client_addr.sin_addr), htons(client_addr.sin_port)); pid_t id = fork();//调用fork实现多进程 if(id == 0){//child close(server_sock); if(fork() > 0){ } char buf[1024]; while(1){ ssize_t s = read(client_sock, buf, sizeof(buf)-1); if(s > 0){ buf[s]=0; printf("client #: %s\n", buf); }else if(s == 0){ printf("client quit\n"); break; }else{ perror("read"); exit(5); } } close(client_sock); }else{//father close(client_sock); while(waitpid(-1, NULL, WNOHANG)); continue; } } return 0; }客户端:

#include<stdio.h> #include<sys/types.h> #include<sys/socket.h> #include<string.h> #include<stdlib.h> #include<netinet/in.h> void usage(const char* proc) { printf("Usage: %s [local_ip] [local_port]\n", proc); } int main(int argc, char* argv[]) { if(argc != 3){ usage(argv[0]); exit(1); } int sock = socket(AF_INET, SOCK_STREAM, 0); if(sock < 0){ perror("socket"); exit(2); } struct sockaddr_in local; local.sin_family = AF_INET; local.sin_port = htons(atoi(argv[2])); local.sin_addr.s_addr = inet_addr(argv[1]); if(connect(sock, (struct sockaddr*)&local, sizeof(local)) < 0){ perror("connect"); exit(3); } printf("connect success!\n"); char buf[1024]; while(1){ printf("client #:"); fflush(stdout); int ret = read(0, buf, sizeof(buf)-1); if(ret <= 0){ perror("read"); exit(4); }else{ buf[ret-1] = 0; write(sock, buf, strlen(buf)); } ret = read(sock, buf, sizeof(buf)-1); if(ret < 0){ perror("read"); exit(5); }else if(ret == 0){ printf("server quit\n"); break; }else{ buf[ret] = 0; printf("server #:%s\n", buf); } } close(sock); return 0; }多进程的特点就在于调用fork()函数处理了一下,让子进程去走,而父进程一直等待。

服务器端可以创建多个子进程去处理客户端发来的信息。当每次收到一个新的客户端的连接请求的时候,我们就会fork()出一个子进程,父进程用于等待子进程,子进程用于执行 读客户端发的数据 的操作。细心的你可能会发现,我们在子进程读取信息之前还进行了一次fork(),这是为什么呢?其实,我们用子进程fork()出一个孙子进程,终止掉儿子进程,儿子进程被它的父进程回收,此时的孙子进程就是一个孤儿进程,被1号进程领养。这样做的目的就是,不要让儿子进程等待孙子进程太久而消耗太多的系统资源。 

另外,这里还涉及到父进程关闭通信套接字,子进程关闭监听套接字。这是因为,父进程是来监听的,不需要通信,子进程是读取信息的,不需要监听。

这里呢运行结果我就不显示了,如果有兴趣你可以自己去试一下。

多线程的套接字TCP通信

上面已经提到了多进程,那么这里我们就不得不提到多线程了。以前的学习中我们知道线程是系统内部的一个执行流,是在进程的地址空间中运行的。而进程是程序的一次动态执行过程,系统中进程过多的话会增加系统的负担,所以就有了多线程的通信。

服务器端:

#include<stdio.h> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #include<arpa/inet.h> #include<string.h> #include<stdlib.h> #include<pthread.h> static void usage(const char* proc) { printf("Usage: %s [local_ip] [local_port]\n", proc); } int startup(const char* _ip, int _port) { int sock = socket(AF_INET, SOCK_STREAM, 0); if(sock < 0){ perror("socket"); exit(2); } struct sockaddr_in local; local.sin_family = AF_INET; local.sin_port = htons(_port);//htons就是将host主机转换为net网络中的,而s代表short,l则代表long local.sin_addr.s_addr = inet_addr(_ip); if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0){ perror("bind"); exit(3); } if(listen(sock, 5)){//5表示监听的最大个数 perror("listen"); exit(4); } return sock; } void* thread_handler(void* arg)//为每一个线程执行客户端的读写 { int sock = (int)arg; printf("sock:%d\n", sock); char buf[1024]; while(1){ ssize_t s = read(sock, buf,sizeof(buf)-1); if(s > 0){ buf[s]=0; printf("client #: %s\n", buf); if(write(sock, buf, sizeof(buf)-1) < 0){ break; } }else if(s == 0){ printf("client quit\n"); break; }else{ perror("read"); break; } } close(sock); } int main(int argc, char* argv[]) { if(argc != 3){ usage(argv[0]); exit(1); } int server_sock = startup(argv[1],atoi(argv[2])); while(1){ struct sockaddr_in client_addr; socklen_t len = sizeof(client_addr); int client_sock = accept(server_sock, (struct sockaddr*)&client_addr, &len); if(client_sock < 0){ perror("accept"); continue; } printf("IP is %s, Port is %d\n", inet_ntoa(client_addr.sin_addr), htons(client_addr.sin_port)); pthread_t tid; int ret = pthread_create(&tid, NULL, thread_handler, (void*)(client_sock));//创建一个新线程来执行客户端的读写操作 if(ret < 0){ perror("pthread_create"); exit(5); } pthread_detach(tid);//设置线程状态为分离状态,这样主线程不用去等待,可以继续执行循环 } close(server_sock); return 0; } 客户端:

#include<stdio.h> #include<sys/types.h> #include<sys/socket.h> #include<string.h> #include<stdlib.h> #include<netinet/in.h> void usage(const char* proc) { printf("Usage: %s [local_ip] [local_port]\n", proc); } int main(int argc, char* argv[]) { if(argc != 3){ usage(argv[0]); exit(1); } int sock = socket(AF_INET, SOCK_STREAM, 0); if(sock < 0){ perror("socket"); exit(2); } struct sockaddr_in local; local.sin_family = AF_INET; local.sin_port = htons(atoi(argv[2])); local.sin_addr.s_addr = inet_addr(argv[1]); if(connect(sock, (struct sockaddr*)&local, sizeof(local)) < 0){ perror("connect"); exit(3); } printf("connect success!\n"); char buf[1024]; while(1){ printf("client #:"); fflush(stdout); int ret = read(0, buf, sizeof(buf)-1); if(ret <= 0){ perror("read"); exit(4); }else{ buf[ret-1] = 0; write(sock, buf, strlen(buf)); } ret = read(sock, buf, sizeof(buf)-1); if(ret < 0){ perror("read"); exit(5); }else if(ret == 0){ printf("server quit\n"); break; }else{ buf[ret] = 0; printf("server #:%s\n", buf); } } close(sock); return 0; }

多线程呢就是在主线程中创建出一个新线程,新线程的执行函数是读取信息。类似于上边的多进程间的通信,我们可以将新的线程进行分离,分离之后的线程就不需要主线程去等待,而是由操作系统区回收。(这里我们不可以join新线程,如果这样做的话,主线程还是需要花费很长的时间去等待,所以,新的线程还是由系统去回收)

补充:server bind失败的原因?

这里呢我们来模拟一种情形:先运行server,再运行client,client给server发数据,然后ctrl+c终止调server,立即再次启动server,会出现什么现象呢?

我们可以看到Address already is in use。

这是什么原因呢?服务器终止程序,服务器就是主动发起断开连接请求的一方,根据TCP的3次握手4次挥手协议(如果有些不清楚请点击这里查看tcp协议的三次握手与四次挥手),主动发起连接断开请求的一方,最后必须等待2MSL的时间确认客户端是否收到自己的确认信息。这里,我们立即运行server的时候,server还是在TIME_WAIT状态,所以bind的时候就会出现地址已经被占用。

解决方法:socket之后,bind之前,加语句 int opt = 1; setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); 就是为了处理这个问题。

其中的sock就是socket函数创建套接字的返回值。

转载请注明原文地址: https://www.6miu.com/read-29642.html

最新回复(0)