Linux下进程间通信-套接字 原创 Linux平台 2022年2月19日 19:36 夏至未至 2653 当前内容 13278 字,在路上,马上到,马上到 ### 套接字介绍 #### 何为套接字 套接字(socket)是网络通信协议中应用层和传输层之间的一个抽象层,它是一组接口,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用,用于实现进程在网络中通信。套接字除了可以实现网络间不同主机间的通信外,还可以实现同一主机的不同进程间的通信,且建立的通信是双向的通信。 #### 套接字分类 分为流式套接字和数据包套接字,分别对应网络传输控制层的`TCP`和`UDP`协议。 - 流套接字(在某些方面类似域标准的输入/输出流)提供的是一个有序,可靠,双向字节流的连接。流套接字由类型SOCK_STREAM指定,它们是在AF_INET域中通过TCP/IP连接实现的。他们也是AF_UNIX域中常见的套接字类型。 - 数据包套接字与流套接字相反,由类型SOCK_DGRAM指定的数据包套接字不建立和维持一个连接。它对可以发送的数据包的长度有限制。数据报作为一个单独的网络消息被传输,它可能会丢失,复制或乱序到达。数据报套接字实在AF_INET域中通过UDP/IP连接实现,它提供的是一种无需的不可靠服务。 #### 套接字的域 域指定套接字通信中使用的网络介质。最常见的套接字域是AF_INET,它是指Internet网络,许多Linux局域网使用的都是该网络,当然,因特网自身用的也是它。其底层的协议——网际协议(IP)只有一个地址族,它使用一种特定的方式来指定网络中的计算机,即IP地址。在计算机系统内部,端口通过分配一个唯一的16位的整数来表示,在系统外部,则需要通过IP地址和端口号的组合来确定。 ### 套接字实现 #### 套接字通信过程 服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。  图是dao的 #### 套接字实现函数 ##### socket 函数 socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作,原型如下: int socket(int domain, int type, int protocol); ###### 入参 1. domain:即协议域/族,协议族决定了socket的地址类型,在通信中必须采用对应的地址 2. type:指定socket类型。SOCK_STREAM 或SOCK_DGRAM,socket接口还定义了原始Socket(SOCK_RAW),允许程序使用低层协议; 3. protocol:指定协议。 > 注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。 ###### 返回值 成功返回的socket描述字,它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。失败返回-1 ##### bind 函数 bind()函数把一个地址族中的特定地址赋给 socket。例如对应 AF_INET、AF_INET6 就是把一个 ipv4 或 ipv6 地址和 端口号组合赋给socket。原型如下: int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); ###### 入参 1. sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。 2. addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同。 struct sockaddr { unsigned short sa_family; // 地址族, AF_xxx char sa_data[14]; // 14 字节的协议地址 }; sa_family 一般为AF_INET,代表Internet(TCP/IP)地址族;sa_data则包含该socket的IP地址和端口号。 另外还有一种结构类型: struct sockaddr_in { short int sin_family; // 地址族 unsigned short int sin_port; // 端口号 struct in_addr sin_addr; // IP地址 unsigned char sin_zero[8]; // 填充0 以保持与struct sockaddr同样大小 }; 这个结构更方便使用。sin_zero用来将sockaddr_in结构填充到与struct sockaddr同样的长度,可以用bzero()或memset()函数将其置为零。指向sockaddr_in 的指针和指向sockaddr的指针可以相互转换,这意味着如果一个函数所需参数类型是sockaddr时,你可以在函数调用的时候将一个指向 sockaddr_in的指针转换为指向sockaddr的指针;或者相反。 使用bind函数时,可以用下面的赋值实现自动获得本机IP地址和随机获取一个没有被占用的端口号: my_addr.sin_port = 0; // 系统随机选择一个未被使用的端口号 my_addr.sin_addr.s_addr = INADDR_ANY; // 填入本机IP地址 通过将my_addr.sin_port置为0,函数会自动为你选择一个未占用的端口来使用。同样,通过将my_addr.sin_addr.s_addr置为INADDR_ANY,系统会自动填入本机IP地址。 注意在使用bind函数是需要将sin_port和sin_addr转换成为网络字节优先顺序;而sin_addr则不需要转换。 计算机数据存储有两种字节优先顺序:高位字节优先和低位字节优先。Internet上数据以高位字节优先顺序在网络上传输,所以对于在内部是以低位字节优先方式存储数据的机器,在Internet上传输数据时就需要进行转换,否则就会出现数据不一致。 下面是几个字节顺序转换函数: ·htonl():把32位值从主机字节序转换成网络字节序 ·htons():把16位值从主机字节序转换成网络字节序 ·ntohl():把32位值从网络字节序转换成主机字节序 ·ntohs():把16位值从网络字节序转换成主机字节序 3. addrlen:对应的是地址的长度。 > 通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。 ###### 返回值 函数在成功被调用时返回0;出现错误时返回"-1"并将errno置为相应的错误号。需要注意的是,在调用bind函数时一般不要将端口号置为小于1024的值,因为1到1024是保留端口号,你可以选择大于1024中的任何一个没有被占用的端口号。 ##### 函数 listen 作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。原型如下: int listen(int sockfd, int backlog); ###### 入参 1. 第一个参数即为要监听的socket描述字 2. 第二个参数为相应socket可以排队的最大连接个数。 > socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。 ###### 返回值 当出现错误时listen函数返回-1,并置相应的errno错误码 ##### 函数 connect 面向连接的客户程序使用Connect函数来配置socket并与远端服务器建立一个TCP连接,其函数原型为: int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); Connect函数启动和远端主机的直接连接。只有面向连接的客户程序使用socket时才需要将此socket与远端主机相连。无连接协议从不建立直接连接。面向连接的服务器也从不启动一个连接,它只是被动的在协议端口监听客户的请求。 ###### 入参 1. 第一个参数即为客户端的socket描述字 2. 第二参数是包含远端主机IP地址和端口号的指针 3. 第三个参数为socket地址的长度 > 客户端通过调用connect函数来建立与TCP服务器的连接。 ###### 返回值 Connect函数在出现错误时返回-1,并且设置errno为相应的错误码,进行客户端程序设计无须调用bind(),因为这种情况下只需知道目的机器 的IP地址,而客户通过哪个端口与服务器建立连接并不需要关心,socket执行体为你的程序自动选择一个未被占用的端口,并通知你的程序数据什么时候到 打断口。 ##### 函数 accept TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就想TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。 int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); ###### 入参 1. 第一个参数为服务器的socket描述字 2. 第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址 3. 第三个参数为协议地址的长度。 > 注意:accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。 ###### 返回值 accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接 ##### 数据读写函数 服务器与客户已经建立好连接了。可以调用网络I/O进行读写操作,即实现了网咯中不同进程之间的通信,网络I/O操作有下面几组: read()/write() recv()/send() readv()/writev() recvmsg()/sendmsg() recvfrom()/sendto() ###### send 和 recv send()和recv()这两个函数用于面向连接的socket上进行数据传输。 send()函数原型为: int send(int sockfd, const void *msg, int len, int flags); Sockfd是你想用来传输数据的socket描述符;msg是一个指向要发送数据的指针;Len是以字节为单位的数据的长度;flags一般情况下置为0。 send()函数返回实际上发送出的字节数,可能会少于你希望发送的数据。在程序中应该将send()的返回值与欲发送的字节数进行比较。当send()返回值与len不匹配时,应该对这种情况进行处理。 recv()函数原型为: int recv(int sockfd,void *buf,int len,unsigned int flags); Sockfd是接受数据的socket描述符;buf 是存放接收数据的缓冲区;len是缓冲的长度。Flags也被置为0。Recv()返回实际上接收的字节数,当出现错误时,返回-1并置相应的errno值。 ###### sendto 和 recvfrom Sendto()和recvfrom()用于在无连接的数据报socket方式下进行数据传输。由于本地socket并没有与远端机器建立连接,所以在发送数据时应指明目的地址。 sendto()函数原型为: int sendto(int sockfd, const void *msg,int len,unsigned int flags,const struct sockaddr *to, int tolen); 该函数比send()函数多了两个参数,to表示目地机的IP地址和端口号信息,而tolen常常被赋值为sizeof (struct sockaddr)。Sendto 函数也返回实际发送的数据字节长度或在出现发送错误时返回-1。 recvfrom()函数原型为: int recvfrom(int sockfd,void *buf,int len,unsigned int flags,struct sockaddr *from,int *fromlen); from是一个struct sockaddr类型的变量,该变量保存源机的IP地址及端口号。fromlen常置为sizeof (struct sockaddr)。当recvfrom()返回时,fromlen包含实际存入from中的数据字节数。Recvfrom()函数返回接收到的字节数或 当出现错误时返回-1,并置相应的errno。 如果对数据报socket调用了connect()函数时,你也可以利用send()和recv()进行数据传输,但该socket仍然是数据报socket,并且利用传输层的UDP服务。但在发送或接收数据报时,内核会自动为之加上目地和源地址信息。 ##### 函数 close 在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。原型: int close(int fd); ##### setsockopt 和 getsockopt 获取或者设置与某个套接字关联的选项。选项可能存在于多层协议中,它们总会出现在最上面的套接字层。当操作套接字选项时,选项位于的层和选项的名称必须给出。为了操作套接字层的选项,应该将值指定为SOL_SOCKET。为了操作其它层的选项,控制选项的合适协议号必须给出。 函数原型: int getsockopt(int sock, int level, int optname, void *optval, socklen_t *optlen); int setsockopt(int sock, int level, int optname, const void *optval, socklen_t optlen); ###### 入参 1. sock:将要被设置或者获取选项的套接字。 2. level:选项所在的协议层。 3. optname:需要访问的选项名。 4. optval:对于getsockopt(),指向返回选项值的缓冲。对于setsockopt(),指向包含新选项值的缓冲。 5. optlen:对于getsockopt(),作为入口参数时,选项值的最大长度。作为出口参数时,选项值的实际长度。对于setsockopt(),现选项的长度。 ###### 返回值 成功执行时,返回0。失败返回-1,errno被设为以下的某个值 EBADF:sock不是有效的文件描述词 EFAULT:optval指向的内存并非有效的进程空间 EINVAL:在调用setsockopt()时,optlen无效 ENOPROTOOPT:指定的协议层不能识别选项 ENOTSOCK:sock描述的不是套接字 #### 代码实现 场景:在同一台机器上进行套接字(socket)两进程间通信,传送图片 ##### server 服务端监听客户端连接,给客户端提供服务 server.c #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; using namespace cv; int main(int argc, char *argv[]) { cout << "https://www.codecomeon.com/" << endl; // 参数检查 if (argc < 4) { cout << "参数不正确" << endl; cout << "https://www.codecomeon.com/" << endl; return -1; } // socket int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { cout << "socket创建失败" << endl; cout << "https://www.codecomeon.com/" << endl; } int reuse; if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(int)) == -1){ printf("Error : %s\n", strerror(errno)); cout << "https://www.codecomeon.com/" << endl; } int optval = 1; if (setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &optval, sizeof(int)) < 0){ cout << "Error set TCP_NODELAY option on listen socket" << endl; } struct sockaddr_in serv_addr, cli_addr; bzero((char *) &serv_addr, sizeof(serv_addr)); int portno = atoi(argv[1]); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = INADDR_ANY; serv_addr.sin_port = htons(portno); // bind if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) { cout << "ERROR on binding" << endl; } // listen listen(sockfd,5); socklen_t clilen = sizeof(cli_addr); int newsockfd = accept(sockfd,(struct sockaddr *) &cli_addr,&clilen); if (newsockfd < 0) { cout << "ERROR on accept" << endl; } // 设置输出 namedWindow( "Server", CV_WINDOW_NORMAL ); int bytes = 0, bytecount = 0; Mat img; int width = atoi(argv[2]); int height = atoi(argv[3]); int imgSize = 3*height*width;//921600; uchar sockData[imgSize]; while(1){ img = Mat::zeros( height,width, CV_8UC3); // MSG_WAITALL bytes = 0; bytecount = 0; for (int i = 0; i < imgSize; i += bytes) { if ((bytes = recv(newsockfd, (sockData + i), imgSize - i, MSG_WAITALL)) == -1) { cout << "recv failed" << endl; return -1; } if (bytes == 0) bytecount++; if ( bytecount == 20) { cout<<"cleanup"<(i,j) = cv::Vec3b(sockData[ptr+ 0],sockData[ptr+1],sockData[ptr+2]); ptr=ptr+3; } } imshow( "Server", img ); waitKey(0); } cout << "https://www.codecomeon.com/" << endl; close(newsockfd); close(sockfd); return 0; } ##### client 用户连接服务端 connect client.c #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; using namespace cv; int main(int argc, char *argv[]) { cout << "https://www.codecomeon.com/" << endl; int portno = atoi(argv[1]); // 创建 socket int my_socked = socket(AF_INET, SOCK_STREAM, 0); if (my_socked < 0) { cout << "创建 socket 失败" << endl; cout << "https://www.codecomeon.com/" << endl; } // 网络(IP/PORT) struct sockaddr_in serv_addr; bzero((char *) &serv_addr, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(portno); serv_addr.sin_addr.s_addr = inet_addr(argv[2]); // 本机地址默认 127.0.0.1 // 重用已经绑定的地址/端口 int optval = 1; if (setsockopt(my_socked, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(int)) < 0) { cout << "Cannot set SO_REUSEADDR option on listen socket " << endl; } // 确定设置 TCP_NODELAY optval = 1; if (setsockopt(my_socked, IPPROTO_TCP, TCP_NODELAY, &optval, sizeof(int)) < 0) { cout << " Cannot set TCP_NODELAY option on listen socket" << endl; } // connect if (connect(my_socked, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) { cout << "ERROR connecting" << endl; } Mat img=imread("/data/socket/test.png", 1); // 图片尺寸 int imgSize =3*img.rows*img.cols; // 发送数据给服务端 int bytes = send(my_socked, img.data, imgSize, 0); // close close(my_socked); } 本文标题: Linux下进程间通信-套接字 本文作者: 夏至未至 发布时间: 2022年2月19日 19:36 最近更新: 2022年3月2日 22:00 原文链接: 许可协议: 署名-非商业性-禁止演绎 4.0 国际(CC BY-NC-ND 4.0) 请按协议转载并保留原文链接及作者 进程间通信(8) 套接字(1) Socket(1) 上一个 Linux任务进度条实现 下一个 Linux进程间通信-信号量 当前文章评论暂未开放,请移步至留言处留言。