socket网络通信

Linux下网络socket编程——实现服务器(select)与多个客户端通信
Linux、windows下使用socket的区别

0、头文件

一、发送接收

read()/write()
recv()/send()    //
readv()/writev()
recvmsg()/sendmsg()
recvfrom()/sendto()   //

推荐使用recvmsg()/sendmsg()函数,这两个函数是最通用的I/O函数,实际上可以把上面的其它函数都替换成这两个函数。它们的声明如下:

 #include <unistd.h>
   ssize_t read(int fd, void *buf, size_t count);
   ssize_t write(int fd, const void *buf, size_t count);

 #include <sys/types.h>
 #include <sys/socket.h>
   ssize_t send(int sockfd, const void *buf, size_t len, int flags);
   ssize_t recv(int sockfd, void *buf, size_t len, int flags);

   ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                  const struct sockaddr *dest_addr, socklen_t addrlen);
   ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                    struct sockaddr *src_addr, socklen_t *addrlen);

   ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
   ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);   

二、半连接时往soket上写,产生SIG_PIPE错误

对一个对端已经关闭(或网线中间断开)的socket调用两次write, 第二次将会生成SIGPIPE信号, 该信号默认结束进程。

具体的分析可以结合TCP的”四次握手”关闭。TCP是全双工的信道, 可以看作两条单工信道, TCP连接两端的两个端点各负责一条。当对端调用close时, 虽然本意是关闭整个两条信道, 但本端只是收到FIN包,按照TCP协议的语义, 表示对端只是关闭了其所负责的那一条单工信道, 仍然可以继续接收数据。 也就是说, 因为TCP协议的限制,一个端点无法获知对端已经完全关闭。
在这里插入图片描述
对一个已经收到FIN包的socket调用read方法, 如果接收缓冲已空,则返回0, 这就是常说的表示连接关闭。但第一次对其调用write方法时, 如果发送缓冲没问题,会返回正确写入(发送)。但发送的报文会导致对端发送RST报文, 因为对端的socket已经调用了close, 完全关闭, 既不发送, 也不接收数据。所以, 第二次调用write方法(假设在收到RST之后), 会生成SIGPIPE信号, 导致进程退出。

结论:

  1. read返回0表示对端连接关闭
  2. 向已断开连接的socket写,本地未感知的情况下,write不会返回0,除非发送长度为0
  3. 上述情况下,第一次write,返回值为write的字节数,且对端返回RST分节,此时 不能识别出连接已断开
  4. 第二次write,本地直接返回SIGPIPE,如果处理了SIGPIPE信号,write返回值为-1,errno为EPIPE
  5. SIGPIPE默认中止进程,所以如果不想进程中止就需要处理SIGPIPE信号,可以忽略或catch住做进一步处理
  6. 对端进程中止的时候会发送FIN分节

三、阻止SIGPIPE

往断开连接的socket写数据引起SigPipe解决方法
解决方法:重新定义遇到SIGPIPE的措施
(一)使用signal(SIGPIPE, SIG_IGN)

signal(SIGPIPE, SIG_IGN);     //交由系统处理---
//系统里边定义了三种处理方法: 
//(1)SIG_DFL    /* Default action */ 
//(2)SIG_IGN    /* Ignore action */ 
//(3)SIG_ERR    /* Error return */ 
void   signal_handle(ing  sigNo)     
{    //do something;     
}      
  
int   main()     
{  
    signal(SIGPIPE, signal_handle);  //自定义处理----
    ......  
}  

如果服务器采用了fork的话,要收集垃圾进程,防止僵尸进程的产生,可以这样处理:

signal(SIGCHLD,SIG_IGN);//交给系统init去回收,这里子进程就不会产生僵尸进程了。

(二)使用sigaction

struct sigaction sa;
sa.sa_handler = SIG_IGN;
sigaction( SIGPIPE, &sa, 0 );
//上面signal设置的信号句柄只能起一次作用,信号被捕获一次后,信号句柄就会被还原成默认值了,
//而sigaction设置的信号句柄,可以一直有效,直到你再次改变它的设置。
struct sigaction sa;
sa.sa_handler = handle_pipe;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGPIPE, &sa, NULL);//用于忽略sigpipe信号
void handle_pipe(int sig)
{
//不做任何处理即可
}

1、 进程中:

	struct sigaction sa;
	sa.sa_handler=handle_pipe;
	sigemptyset(&sa.sa_mask);
	sa.sa_flags=0;
	sigaction(SIGPIPE,&sa,0);/用于忽略sigpipe信号
	struct sigaction sa; 
	sa.sa_handler = SIG_IGN;  //只一次作用
	sa.sa_flags = 0; 
	if((sigemptyset(&sa.sa_mask) == -1) || sigaction(SIG_PIPE, &sa, 0) == -1) 
	{ 
		perror("SIGPIPE"); 
		exit(EXIT_FAILURE) 
	} 

这种方法对于进程可以忽略SIGPIPE。
但是POSIX的线程,对信号的捕获是逐条线程进行的,所以引出了下面的解决方案。

2、 多线程中:
代码应写在main函数的开头,创建线程之前。

	sigset_t  signal_mask; 
	sigemptyset(&signal_mask); 
	sigaddset(&signal_mask, SIG_PIPE); 
	if(pthread_sigmask(SIG_BLOCK, &signal_mask, NULL) == -1) 
		perror("SIG_PIPE"); 

这样就能屏蔽掉线程中的SIG_PIPE。

	sigset_t signal_mask;
	sigemptyset (&signal_mask);
	sigaddset (&signal_mask, SIGPIPE);
	int rc = pthread_sigmask (SIG_BLOCK, &signal_mask, NULL);
	if (rc != 0)
	{
	   printf("block sigpipe error\n");
	} 

3、send参数指定MSG_NOSIGNAL

	int ret=send(client_fds[i], send_message, BUFF_SIZE, MSG_NOSIGNAL);							
	if(ret<=0)//send失败则关闭该socket
	{
		printf("----This is socket send failure! (ret<=0)----- \n");
		close(client_fds[i]);
		FD_CLR(client_fds[i], &ser_fdset);
		client_fds[i] = 0;
	}

3、关闭线程
线程的关闭
检测一个线程是否还活着的pthread函数

int pthread_kill(pthread_t thread, int sig)
向指定ID的线程发送sig信号,如果线程的代码内不做任何信号处理,则会按照信号默认的行为影响整个进程。也就是说,如果你给一个线程发送了SIGQUIT,但线程却没有实现signal处理函数,则整个进程退出。
pthread_kill(threadid, SIGKILL)也一样,他会杀死整个进程。
如果要获得正确的行为,就需要在线程内实现signal(SIGKILL,sig_handler)。
所以,如果int sig的参数不是0,那一定要清楚到底要干什么,而且一定要实现线程的信号处理函数,否则,就会影响整个进程。
那么,如果int sig的参数是0呢,这是一个保留信号,一个作用就是用来判断线程是不是还活着。
我们来看一下pthread_kill的返回值:
线程仍然活着:0
线程已不存在:ESRCH
信号不合法:EINVAL
4、安全发送
socket安全发送

#include <unistd.h>
#include <time.h>
#include <errno.h>
#include <sys/signal.h>

ssize_t safe_write(int fd, const void* buf, size_t bufsz)
{
    sigset_t sig_block, sig_restore, sig_pending;

    sigemptyset(&sig_block);
    sigaddset(&sig_block, SIGPIPE);

    /* Block SIGPIPE for this thread.
     *
     * This works since kernel sends SIGPIPE to the thread that called write(),
     * not to the whole process.
     */
    if (pthread_sigmask(SIG_BLOCK, &sig_block, &sig_restore) != 0) {
        return -1;
    }

    /* Check if SIGPIPE is already pending.
     */
    int sigpipe_pending = -1;
    if (sigpending(&sig_pending) != -1) {
        sigpipe_pending = sigismember(&sig_pending, SIGPIPE);
    }

    if (sigpipe_pending == -1) {
        pthread_sigmask(SIG_SETMASK, &sig_restore, NULL);
        return -1;
    }

    ssize_t ret;
    while ((ret = write(fd, buf, bufsz)) == -1) {
        if (errno != EINTR)
            break;
    }

    /* Fetch generated SIGPIPE if write() failed with EPIPE.
     *
     * However, if SIGPIPE was already pending before calling write(), it was
     * also generated and blocked by caller, and caller may expect that it can
     * fetch it later. Since signals are not queued, we don't fetch it in this
     * case.
     */
    if (ret == -1 && errno == EPIPE && sigpipe_pending == 0) {
        struct timespec ts;
        ts.tv_sec = 0;
        ts.tv_nsec = 0;

        int sig;
        while ((sig = sigtimedwait(&sig_block, 0, &ts)) == -1) {
            if (errno != EINTR)
                break;
        }
    }

    pthread_sigmask(SIG_SETMASK, &sig_restore, NULL);
    return ret;
}

5、发送之前先读一下来判断socket是否关闭
摘自《UNIX网络编程 卷1》6.7

int n;
if((n = Read(sockfd, buf, MAXLINE) == 0)
{
	//断开了,关闭socket
}
else
{
	//处理接收数据
}

四、udp

3.1 简单例子

struct socketaddr_in peer_addr;
peer_addr.sin_family = AF_INET;
peer_addr.sin_port = htons(10030);
peer_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
  • htons()作用是将端口号由主机字节序转换为网络字节序的整数值。(host to net)
  • inet_addr()作用是将IP字符串转化为网络字节序的整数值,用于sockaddr_in.sin_addr.s_addr。
  • inet_ntoa()作用是将一个sin_addr结构体输出成IP字符串(network to ascii)。比如:printf(“%s”,inet_ntoa(mysock.sin_addr));

htonl()是32位的(long),ntohl()
htons()是16位的(short),ntohs()。

3.2 udp的connect

对UDP套接字调用connect函数,主要会在内核中记录目标的IP地址和端口,后续读取数据或者发送数据不用调用sendto/recvfrom,可以直接调用recv/send函数

五 、组播

socket之UDP组播
linux下实现组播(socket)
linux下组播的发送和接收
基于Winsock的UDP组播通信入门
1、组播地址
为d类地址,即224.0.0.0到239.255.255.255为组播地址。
其中:

  • 224.0.0.0到224.0.0.255为预留地址;
  • 224.0.1.0到238.255.255.255为组播地址,全网有效;
  • 239.0.0.0到239.255.255.255为本地地址(类似192.168地址),仅特定的本地范围内有效,包只在本地子网中传播;

2、开启组播
广播和组播属性默认都是关闭的,如果使用需要通过 setsockopt () 函数进行设置,即:创建了SOCK_DGRAM类型的socket以后,通过调用setsockopt()函数来控制该socket的组播。

windows中socket绑定网卡对应的ip后可以正常接收发送到该网卡的多播数据包,并且通过该socket发送的多播数据包也是通过绑定的网卡进行发送的。Linux中socket绑定网卡对应的ip后无法接收到发送到该网卡的多播数据包,必须绑定0.0.0.0或者多播地址才能接收到对应的多播数据包。

#include <sys/socket.h>
setsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
 当level = IPPROTO_IP 时 ,optval有:
 - IP_ADD_MEMBERSHIP  加入指定的组播组。
 - IP_DROP_MEMBERSHIP  离开指定的组播组。
 - IP_MULTICAST_IF   指定发送组播数据的网络接口。
 - IP_MULTICAST_TTL   给出发送组播数据时的TTL,默认是1- IP_MULTICAST_LOOP  发送组播数据的主机是否作为接收组播数据的组播成员。
//发送举例----------------------
//本地开启组播
struct in_addr local_addr;
local_addr.s_addr = inet_addr("203.106.93.94"); //inet_pton(AF_INET, GROUP_IP, &local_addr.s_addr);
setsockopt(sd, IPPROTO_IP, IP_MULTICAST_IF, (char *)&local_addr, sizeof(local_addr))//往组播地址发送数据
struct sockaddr_in   group_addr;
memset((char *) &group_addr, 0, sizeof(group_addr));
group_addr.sin_family = AF_INET;
group_addr.sin_addr.s_addr = inet_addr("226.1.1.1");
group_addr.sin_port = htons(4321);
sendto(sd, databuf, datalen, 0, (struct sockaddr*)&group_addr, sizeof(group_addr));

//接收举例----------------------
//绑定本地端口
struct sockaddr_in   local_addr;
memset((char *) &local_addr, 0, sizeof(local_addr));
local_addr.sin_family = AF_INET;
local_addr.sin_port = htons(49500);
local_addr.sin_addr.s_addr = INADDR_ANY;
bind(sd, (struct sockaddr*)&local_addr, sizeof(local_addr));
//加入组播组,使用 ip_mreq
struct ip_mreq group;   //ipmreqn结构多了个
group.imr_multiaddr.s_addr = inet_addr("227.0.0.25");    //组播地址
group.imr_interface.s_addr = inet_addr("150.158.231.2"); //本地IP   htonl(INADDR_ANY);
setsockopt(sd, IPPROTO_IP, IP_ADD_MEMBERSHIP, (char *)&group, sizeof(group));
//加入组播组,使用 ip_mreqn
 struct ip_mreqn group;   
 inet_pton(AF_INET, GROUP_IP, &group.imr_multiaddr.s_addr); //组播地址
 group.imr_address.s_addr = htonl(INADDR_ANY);   //本地IP
 group.imr_ifindex = if_nametoindex("ens33");   //网卡名称
 setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &group, sizeof(group));

//接收
read(sd, databuf, datalen);

3、发送组播
步骤:
①创建AF_INET, SOCK_DGRAM的socket。
②用组播IP地址和端口初始化sockaddr_in类型数据。
③IP_MULTICAST_LOOP,设置本机是否作为组播组成员接收数据。
④IP_MULTICAST_IF,设置发送组播数据的端口。
⑤发送组播数据。

#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>

struct in_addr localInterface;
struct sockaddr_in groupSock;
int sd;
char databuf[1024] = "Multicast test message lol!";
int datalen = sizeof(databuf);

int main (int argc, char *argv[ ])
{
     sd = socket(AF_INET, SOCK_DGRAM, 0);
     if(sd < 0) {
          perror("Opening datagram socket error");
          exit(1);
     } else
          printf("Opening the datagram socket...OK.\n");   

	 //本地开启组播
     localInterface.s_addr = inet_addr("203.106.93.94");
     if(setsockopt(sd, IPPROTO_IP, IP_MULTICAST_IF, (char *)&localInterface, sizeof(localInterface)) < 0)
     {
        perror("Setting local interface error");
        exit(1);
     } 
     else
        printf("Setting the local interface...OK\n");
        
	 //往组播地址发送数据
	 memset((char *) &groupSock, 0, sizeof(groupSock));
     groupSock.sin_family = AF_INET;
     groupSock.sin_addr.s_addr = inet_addr("226.1.1.1");
     groupSock.sin_port = htons(4321);
     if(sendto(sd, databuf, datalen, 0, (struct sockaddr*)&groupSock, sizeof(groupSock)) < 0) 
     {
        perror("Sending datagram message error");
     }
     else
        printf("Sending datagram message...OK\n");

     return 0;
}

4、接收组播
步骤:
①创建AF_INET, SOCK_DGRAM类型的socket。
②设定 SO_REUSEADDR,允许多个应用绑定同一个本地端口接收数据包。
③用bind绑定本地端口,IP为INADDR_ANY,从而能接收组播数据包。
④采用 IP_ADD_MEMBERSHIP加入组播组,需针对每个端口采用IP_ADD_MEMBERSHIP。
⑤接收组播数据包。

#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>


struct sockaddr_in localSock;
struct ip_mreq group;
int sd;
int datalen;
char databuf[1500];


int main(int argc, char *argv[])
{
    sd = socket(AF_INET, SOCK_DGRAM, 0);
    if(sd < 0)
    {
        perror("Opening datagram socket error");
        exit(1);
    } else
        printf("Opening datagram socket....OK.\n");
  
    {
        int reuse = 1;
        if(setsockopt(sd, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(reuse)) < 0){
            perror("Setting SO_REUSEADDR error");
            close(sd);
            exit(1);
        } else
            printf("Setting SO_REUSEADDR...OK.\n");
    }

	//绑定本地端口
    memset((char *) &localSock, 0, sizeof(localSock));
    localSock.sin_family = AF_INET;
    localSock.sin_port = htons(49500);
    localSock.sin_addr.s_addr = INADDR_ANY;
    if(bind(sd, (struct sockaddr*)&localSock, sizeof(localSock))){
        perror("Binding datagram socket error");
        close(sd);
        exit(1);
    } else
        printf("Binding datagram socket...OK.\n");
  	//开启组播
    group.imr_multiaddr.s_addr = inet_addr("227.0.0.25");
    group.imr_interface.s_addr = inet_addr("150.158.231.2");
    if(setsockopt(sd, IPPROTO_IP, IP_ADD_MEMBERSHIP, (char *)&group, sizeof(group)) < 0){
        perror("Adding multicast group error");
        close(sd);
        exit(1);
    } else
        printf("Adding multicast group...OK.\n");
    //接收组播数据
    datalen = sizeof(databuf);
    if(read(sd, databuf, datalen) < 0){
        perror("Reading datagram message error");
        close(sd);
        exit(1);
    } else {
        printf("Reading datagram message...OK.\n");
        printf("The message from multicast server is: %d\n", datalen);
    }

    return 0;
}

六、心跳

自适应心跳

七、Winsock

C++利用Socket实现主机间的UDP/TCP通信
有两个版本:Winsock1、Winsock2

#include <WinSock.h>     wsock32.lib
#include <WinSock2.h>    ws2_32.lib
-lwpcap -lwsock32 -liphlpapi -lPsapi
//编译使用C++11以上编译,链接时加入库:
-lwsock32

windows.h会包含winsock.h,和winsock2.h冲突,解决方法:

#include <winsock2.h>
#define WIN32_LEAN_AND_MEAN			//去除windows.h中的winsock.h
#include <Windows.h>
#ifdef _WIN32 	
 	#include <winsock2.h>
 	#pragma comment(lib,"ws2_32.lib")
	#include <stdio.h>
	#include <windows.h>	
	#include <tchar.h>
	#include<ws2tcpip.h>
#else 
	#include <sys/socket.h>
	#include <netinet/in.h>
	#include <sys/types.h>
	#include <netdb.h>
	#include <errno.h>
	#include <fcntl.h>
	#include <unistd.h>
	typedef int SOCKET; 
#endif
#ifdef _WIN32
    WSADATA wsd;
    if(WSAStartup(MAKEWORD(2, 2), &wsd)){
        std::cout << "WSAStartup Error" << std::endl;
        exit(-1);
    }   
#endif
#ifdef _WIN32    
    closesocket(udpSocket);
    WSACleanup();
#else
    close(udpSocket);
#endif
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值