Linux下网络socket编程——实现服务器(select)与多个客户端通信
Linux、windows下使用socket的区别
C++解决TCP粘包(线程接收)
C++实现udp分包和组包
C/C++socket网络编程
C语言实现tcp客户端和tcp服务端,Qt调用测试(线程接收)
0、TCP套件
window下的问题:
1、帧头不能用char,用quint8,否则判断相等不成立。
原因是char在windows上默认是singned char,kylin上默认是unsigned char
quint8 Head[5];
if(pHead->Head[0] == 0x66)
2、common.cpp
#ifdef Q_OS_WIN
#include <io.h>
#else
#include <sys/uio.h>
#endif
3、服务端取本地ip取成监听socket的本地ip,否则发送不成功
//tcpdelegate.cpp
void CServerDelegate::onConnection()
{
//本地地址(服务器地址)
li.uLocalPort = m_naListenAddr.ToPort();
QByteArray baLocalIp = m_naListenAddr.ToIp.toLatin1();
qstrnpy(li.szLocalIp, baLocalIp.data(), sizeof(li.szLocalIp));
}
4、连接
void CConnect::connect()
int nSockFD = CSock::create();
int nRet = CSock::Connect(nSockFD, &sa);
switch(nRet)
{
connecting(nSockFD);
}
void CConnector::connecting(int nSockFD)
m_spChannel.reset(new CChannel(m_pDisp, nSockFD)); //保存socket
一、发送接收
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信号, 导致进程退出。
结论:
- read返回0表示对端连接关闭
- 向已断开连接的socket写,本地未感知的情况下,write不会返回0,除非发送长度为0
- 上述情况下,第一次write,返回值为write的字节数,且对端返回RST分节,此时 不能识别出连接已断开
- 第二次write,本地直接返回SIGPIPE,如果处理了SIGPIPE信号,write返回值为-1,errno为EPIPE
- SIGPIPE默认中止进程,所以如果不想进程中止就需要处理SIGPIPE信号,可以忽略或catch住做进一步处理
- 对端进程中止的时候会发送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
8、各种函数的头文件
//------------------------------------------------------
#include <ws2tcpip.h> //win
#include <sys/types.h> //linux
#include <sys/socket.h>
#include <arpa/inet.h>
inet_ntop() //ip地址:整数转字符串
inet_pton() //ip地址:字符串转整数
//-------------------------------------------------
#include <WINSOCK2.h> //win #pragma comment(lib,"WS2_32.LIB")
#include <arpa/inet.h> //linux
inet_ntod() //ip地址:整数转字符串
inet_addr() //ip地址:字符串转整数
//-------------------------------------------------
#include <> //win
#include <sys/socket.h> //linux
struct sockaddr;
ina.sin_addr.s_addr = inet_addr("132.241.5.10"); //sockaddr
//-------------------------------------------------
#include <> //win
#include<netinet/in.h>或#include <arpa/inet.h> //linux
struct sockaddr_in;
9、sockaddr、sockaddr_in
sockaddr和sockaddr_in详解
二者长度一样,都是16个字节,可相互转化。sockaddr_in用于socket定义和赋值;sockaddr用于函数参数。
- sockaddr:常用于bind、connect、recvfrom、sendto等函数的参数,指明地址信息,是一种通用的套接字地址。
- sockaddr_in :一般先把sockaddr_in变量赋值后,强制类型转换后传入用sockaddr做参数的函数。
9.1 sockaddr
struct sockaddr
{
sa_family_t sin_family; //地址族
char sa_data[14]; //14字节,包含套接字中的目标地址和端口信息
};
9.2 sockaddr_in

10、绑定ip、port
void connect()
{
struct sockaddr sa = m_naServerAddr.GetAddress();
int nSockID = CSock::Create();
#if 0
char ip[16] = {0};
inet_ntop(AF_INET, &((struct sockaddr_in*)&sa)->sin_addr.s_addr, ip, 16);
QString strServerIp = ip;
#else
QString strServerIp = inet_ntoa(((struct sockaddr_in*)&sa)->sin_addr);
#endif
quint16 uPort = ntohs(((struct struct sockaddr_in*)&sa)->sin_port);
if(strServerIp == "192.168.1.66" && uPort == 5000)
{
QString strLocalIp = "192.168.1.88";
quint16 uLocalPort = 6000;
struct sockaddr saLocal;
QByteArray baIp = strLocalIp.toLatin1();
((struct struct sockaddr_in*)&saLocal)->sin_family = AF_INET;
((struct struct sockaddr_in*)&saLocal)->sin_addr.s_addr = inet_addr(baIp.data());
((struct struct sockaddr_in*)&saLocal)->sin_port = htos(uLocalPort);
::bind(nSockFD, &saLocal, sizeof(saLocal));
}
int nRet = CSock::connect(nSockFD, &sa) ;
}
11、绑定网卡
