第一章 TCP网络编程
socket创建套接字
#include <sys/types.h> #include <sys/socket.h> int socket(int domain, int type, int protocol); |
功能
创建网络套接字,用于网络通信使用,类似于文件操作的open函数。该函数在服务器和客户端都会用到。
参数
- int domain :网络协议版本指定。AF_INET IPv4 Internet protocols
AF_INET6 IPv6 Internet protocols - int type:指定通信协议类型。SOCK_STREAM 表明我们用的是TCP协议 (字节流)
SOCK_DGRAM 表明我们用的是UDP协议 (数据报) - int protocol:指定通信协议类型。Type参数已经指定了协议,该参数直接填0即可!
图1-1
返回值
成功返回网络套接字,与open函数返回值类似。
示例
Clientfd = socket(PF_INET,SOCK_STREAM,0); |
bind绑定IP-端口
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen); |
功能
创建服务器。该函数在服务器端使用。
参数
- int sockfd : 网络套接字
- const struct sockaddr *addr :填充创建服务器所需的地址信息,详细的成员看1.3章节。
- socklen_t addrlen :地址长度,就是该结构体的大小。使用sizeof函数进行计算。
返回值
0表示成功,-1表示失败!
struct sockaddr地址结构体
1.3.1 结构体成员解析
在实际填充参数的过程中,struct sockaddr结构体被struct sockaddr_in结构体代替。struct sockaddr_in结构体比struct sockaddr可读性强一些,填充参数比较好理解。
struct sockaddr_in和struct sockaddr大小相同。在填充结构体的时候为了方便填充参数,使用struct sockaddr_in结构体,给函数赋值的时候需要强制转换为struct sockaddr类型的结构体。因为底层函数最终还是使用struct sockaddr类型的结构体。
- struct sockaddr结构体成员:
struct sockaddr { sa_family_t sa_family; //网络协议版本。填写:AF_INET 或者 AF_INET6。 char sa_data[14]; //IP地址和端口 } |
- struct sockaddr_in结构体成员:查看IPV4协议帮助文档:# man 7 ip
struct sockaddr_in { sa_family_t sin_family; /* address family: AF_INET 协议类型*/ in_port_t sin_port; /* port in network byte order 端口号*/ struct in_addr sin_addr; /* internet address 存放IP地址的结构体*/ }; /* Internet address. */ struct in_addr { uint32_t s_addr; /* address in network byte order IP地址 */ }; |
1.3.2 端口号赋值
计算机数据存储有两种字节优先顺序:高位字节优先和低位字节优先。 Internet 上数据以高位字节优先顺序在网络上传输, 所以对于在内部是以低位字节优先方式存储数据的机器, 在 Internet 上传输数据时就需要进行转换, 否则就会出现数据不一致。
普通人用的桌面电脑,只要是Intel或AMD的x86/x64架构就一定是小端字节序。
外很多ARM CPU可以选择数据指令字节序,不过通常也都是运行小端字节序(比如我们的智能手机)。
网络设备,像PowerPC核心的一些路由器,默认运行大端字节序。
下面是几个字节顺序转换函数:
·htonl(): 把 32 位值从主机字节序转换成网络字节序
·htons(): 把 16 位值从主机字节序转换成网络字节序
·ntohl(): 把 32 位值从网络字节序转换成主机字节序
·ntohs(): 把 16 位值从网络字节序转换成主机字节序
函数原型
#include <arpa/inet.h> uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort); 网际协议在处理这些多字节整数时,使用大端字节序。 在主机本身就使用大端字节序时,这些函数通常被定义为空宏。 |
给struct sockaddr_in结构体的端口成员赋值的时候就需要用到以上大端转小端函数进行转换!
示例:
/*结构体成员赋值*/ tcp_server.sin_family=AF_INET; //IPV4协议类型 tcp_server.sin_port=htons(tcp_server_port);//端口号赋值,将本地字节序转为网络字节序 tcp_server.sin_addr.s_addr=INADDR_ANY; //将本地IP地址赋值给结构体成员 //inet_addr("192.168.18.3"); //IP地址赋值 |
1.3.3 IP地址赋值
struct sockaddr_in结构体存放IP地址的成员是struct in_addr 结构体类型,底层存放地址的成员是一个无符号int类型,而我们生活中的IP地址是使用xxx.xxx.xxx.xxx 这种格式表示的。比如:192.168.1.1。 在赋值的时候就需要进行将”192.168.1.1”这种格式转为无符号int类型才能进行赋值。
以下是几个IP格式转换函数:
- 将字符串类型IP转为in_addr_t类型(unsigned int)返回。
in_addr_t inet_addr(const char *cp); |
示例:
Serveraddr.sin_addr.s_addr = inet_addr("192.168.18.3");
- 使用字符串类型的IP直接给结构体成员赋值
int inet_aton(const char *cp, struct in_addr *inp); |
示例:
inet_aton(“192.168.18.3”,&Clientaddr.sin_addr);
- 将结构体里的IP地址成员转为字符串类型返回
char *inet_ntoa(struct in_addr in); |
该函数与上面两个函数功能刚好相反。是将整型的IP转为字符串类型!
1.3.4 本地计算机大小端判断
首先说明,电脑大小端指的是一种存储模式。
1.为什么有大小端:
在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为 8bit。但是在C语言中除了8bit的char之外,还有16bit的short型,32bit的long型(要看具体的编译器),另外,对于位数大于 8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题,因此就导致了大端存储模式和小端存储模式。
2.大小端定义:
大端模式(Big-endian),是指数据的高字节,保存在内存的低地址中,而数据的低字节,保存在内存的高地址中。
小端模式(Little-endian),是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中。
3.直接来看一个图,详细说明大小端:
例子:int i = 0x12345678 两种模式存入内存:
4. 判断大小端的C语言代码
#include<stdio.h> int CheckSystem() { unioncheck { int i; char ch; }c; c.i=1; return (c.ch==1); } int main() { int check=CheckSystem(); if(check==1) printf("当前系统为小端\n"); else printf("当前系统为大端\n"); return 0; } /////////////////////////////////////////////////////////////// // 公用的四个字节地址:0x1001 -> 0x1002 -> 0x1003 -> 0x1004 // 小端来说赋值 1 : 0x01 0x00 0x00 0x00 // 大端来说赋值 1 : 0x00 0x00 0x00 0x01 //也就是说存数据都是从低地址存放一个char字节, //他和int开始的地址是一样的读的话还是从低字节向高字节完整的读取 |
listen监听端口的数量
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int listen(int sockfd, int backlog); |
功能
设置服务器需要监听的端口数量。决定了能够连接的服务器数量。
返回值
成功返回0,失败返回-1。
服务器创建,函数调用顺序:
图1-2
- 示例:listen(Serverfd,10);
accept 等待客户端连接
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); |
功能
以阻塞的形式等待客户端连接。
参数
struct sockaddr *addr :存放已经连接的客户端信息。传入一个结构体地址。
socklen_t *addrlen :表示客户端的结构体大小。该大小需要我们指定,客户端连接成功然后再判断是否与填写的大小一致。
返回值
成功将返回客户端的网络套接字。错误返回-1。
- 示例:
struct sockaddr_in Clientaddr; len = sizeof(struct sockaddr); Clientfd = accept(Serverfd,(struct sockaddr *)&Clientaddr,&len); |
connect连接服务器
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen); |
功能
连接到指定服务器。该函数在客户端使用。
参数
int sockfd :socket函数的网络套接字。
const struct sockaddr *addr :服务器的IP地址信息。 参考:1.2节和1.3.节
socklen_t addrlen :结构体的大小。
返回值
成功返回0,错误返回-1。
- 示例
connect(Clientfd,(struct sockaddr *)&Clientaddr,sizeof(struct sockaddr)); |
send/ recv网络数据收发
#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); |
功能
客户端与服务器之间的数据收发。
参数
const void *buf 、void *buf :读写的缓冲区。
int flags :填0。
- 以上两个函数可以使用write和read函数替换。
shutdown关闭连接
#include <sys/socket.h> int shutdown(int sockfd, int how); |
返回
0—成功,-1—失败。
参数how的值:
SHUT_RD:关闭连接的读这一半,不再接收套接口中的数据且留在套接口缓冲区中的数据都作废。进程不能再对套接口任何读函数。调用此函数后,由TCP套接口接收的任何数据都被确认,但数据本身被扔掉。
SHUT_WR:关闭连接的写这一半,在TCP场合下,这称为半关闭。当前留在套接口发送缓冲区中的数据都被发送,后跟正常的TCP连接终止序列。此半关闭不管套接口描述字的访问计数是否大于0。进程不能再执行对套接口的任何写函数。
SHUT_RDWR:连接的读这一半和写这一半都关闭。这等效于调用shutdown两次:第一次调用时用SHUT_RD,第二次调用时用SHUT_WR。
shutdown(tcp_client_fd,SHUT_WR); //TCP半关闭,保证缓冲区内的数据全部写完 |
- 直接强制关闭连接示例:
int close(int fd); |
1.9 查看Linux系统当前的网络连接
在/proc/net/tcp目录下面保存了当前系统所有TCP链接的状态信息。
查看示例:
[root@wbyq FileSend2]# cat /proc/net/tcp sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode 0: 00000000:006F 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 13264 1 c16ac5c0 99 0 0 10 -1 1: 00000000:DA10 00000000:0000 0A 00000000:00000000 00:00000000 00000000 29 0 13592 1 c16ac0c0 99 0 0 10 -1 2: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 14400 1 c16acac0 99 0 0 10 -1 3: 0100007F:0277 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 13851 1 c142f080 99 0 0 10 -1 4: 0100007F:0019 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 14753 1 c142fa80 99 0 0 10 -1 5: 813DA8C0:A019 49AAC3CB:0522 01 00000000:00000000 00:00000000 00000000 0 0 123641 1 c142f580 20 3 18 10 -1 |
说明: 这里的IP地址信息和端口号都是使用十六进制保存的。
813DA8C0:A019 49AAC3CB:0522表示:
- 查看网络状态连接:
[root@wbyq FileSend2]# netstat -ntp Active Internet connections (w/o servers) Proto Recv-Q Send-Q Local AddressForeign Address State PID/Program name tcp 0 0 192.168.61.129:40985203.195.170.73:1314 ESTABLISHED 20955/./app_c |
从上面可得到的信息:
连接类型: TCP协议
本地IP地址和端口号: 192.168.61.129:40985
与其通信的远程IP地址和端口号: 203.195.170.73:1314
状态: ESTABLISHED(已建立的连接)
进程PID号与应用程序名称: 20955/./app_c
- socket网络连接的状态如下
1、LISTENING状态
FTP服务启动后首先处于侦听(LISTENING)状态。
2、ESTABLISHED状态
ESTABLISHED的意思是建立连接。表示两台机器正在通信。
3、CLOSE_WAIT
对方主动关闭连接或者网络异常导致连接中断,这时我方的状态会变成CLOSE_WAIT 此时我方要调用close()来使得连接正确关闭
4、TIME_WAIT
我方主动调用close()断开连接,收到对方确认后状态变为TIME_WAIT。TCP协议规定TIME_WAIT状态会一直持续2MSL(即两倍的分 段最大生存期),以此来确保旧的连接状态不会对新连接产生影响。处于TIME_WAIT状态的连接占用的资源不会被内核释放,所以作为服务器,在可能的情 况下,尽量不要主动断开连接,以减少TIME_WAIT状态造成的资源浪费。
目前有一种避免TIME_WAIT资源浪费的方法,就是关闭socket的LINGER选项。但这种做法是TCP协议不推荐使用的,在某些情况下这个操作可能会带来错误。
5、SYN_SENT状态
SYN_SENT状态表示请求连接,当你要访问其它的计算机的服务时首先要发个同步信号给该端口,此时状态为SYN_SENT,如果连接成功了就变为 ESTABLISHED,此时SYN_SENT状态非常短暂。但如果发现SYN_SENT非常多且在向不同的机器发出,那你的机器可能中了冲击波或震荡波 之类的了。这类为了感染别的计算机,它就要扫描别的计算机,在扫描的过程中对每个要扫描的计算机都要发出了同步请求,这也是出现许多 SYN_SENT的原因。
根据TCP协议定义的3次握手断开连接规定,发起socket主动关闭的一方 socket将进入TIME_WAIT状态,TIME_WAIT状态将持续2个MSL(Max Segment Lifetime),在Windows下默认为4分钟,即240秒,TIME_WAIT状态下的socket不能被回收使用. 具体现象是对于一个处理大量短连接的服务器,如果是由服务器主动关闭客户端的连接,将导致服务器端存在大量的处于TIME_WAIT状态的socket, 甚至比处于Established状态下的socket多的多,严重影响服务器的处理能力,甚至耗尽可用的socket,停止服务. TIME_WAIT是TCP协议用以保证被重新分配的socket不会受到之前残留的延迟重发报文影响的机制,是必要的逻辑保证.
第二章 UDP网络编程
2.1 UDP协议创建流程
数据报收发函数
2.2.1 recvfrom函数
UDP使用recvfrom()函数接收数据,他类似于标准的read(),但是在recvfrom()函数中要指明数据的目的地址。
#include <sys/types.h> #include <sys/socket.h> ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr * from, size_t *addrlen); |
返回值
成功返回接收到数据的长度,负数失败
前三个参数等同于函数read()的前三个参数,flags参数是传输控制标志。最后两个参数类似于accept的最后两个参数(接收客户端的IP地址)。
2.2.2 sendto函数
UDP使用sendto()函数发送数据,他类似于标准的write(),但是在sendto()函数中要指明目的地址。
#include <sys/types.h> #include <sys/socket.h> ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr * to, int addrlen); |
返回值
成功返回发送数据的长度,失败返回-1
前三个参数等同于函数read()的前三个参数,flags参数是传输控制标志。参数to指明数据将发往的协议地址,他的大小由addrlen参数来指定。
第三章设置Socket套接字属性
3.1 函数原型介绍
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int getsockopt(int sockfd, int level, int optname,void *optval, socklen_t *optlen); int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen); |
参数
sockfd:标识一个套接口的描述字。
level:选项定义的层次;目前仅支持SOL_SOCKET和IPPROTO_TCP层次。
optname:需设置的选项。
optval:指针,指向存放选项值的缓冲区。
optlen:optval缓冲区的长度。
3.2 属性功能注释
setsockopt()函数用于任意类型、任意状态套接口的设置选项值。尽管在不同协议层上存在选项,但本函数仅定义了最高的“套接口”层次上的选项。选项影响套接口的操作,诸如加急数据是否在普通数据流中接收,广播数据是否可以从套接口发送等等。
- setsockopt()支持的选项定义位置:/usr/include/asm-generic/socket.h
#ifndef __ASM_GENERIC_SOCKET_H #define __ASM_GENERIC_SOCKET_H #include <asm/sockios.h> /* For setsockopt(2) */ #define SOL_SOCKET1 #define SO_DEBUG1 #define SO_REUSEADDR2 #define SO_TYPE3 #define SO_ERROR4 #define SO_DONTROUTE5 #define SO_BROADCAST6 #define SO_SNDBUF7 #define SO_RCVBUF8 #define SO_SNDBUFFORCE32 #define SO_RCVBUFFORCE33 #define SO_KEEPALIVE9 #define SO_OOBINLINE10 #define SO_NO_CHECK11 #define SO_PRIORITY12 #define SO_LINGER13 #define SO_BSDCOMPAT14 /* To add :#define SO_REUSEPORT 15 */ #ifndef SO_PASSCRED /* powerpc only differs in these */ #define SO_PASSCRED16 #define SO_PEERCRED17 #define SO_RCVLOWAT18 #define SO_SNDLOWAT19 #define SO_RCVTIMEO20 #define SO_SNDTIMEO21 #endif /* Security levels - as per NRL IPv6 - don't actually do anything */ #define SO_SECURITY_AUTHENTICATION22 #define SO_SECURITY_ENCRYPTION_TRANSPORT23 #define SO_SECURITY_ENCRYPTION_NETWORK24 #define SO_BINDTODEVICE25 /* Socket filtering */ #define SO_ATTACH_FILTER26 #define SO_DETACH_FILTER27 #define SO_PEERNAME28 #define SO_TIMESTAMP29 #define SCM_TIMESTAMPSO_TIMESTAMP #define SO_ACCEPTCONN30 #define SO_PEERSEC31 #define SO_PASSSEC34 #define SO_TIMESTAMPNS35 #define SCM_TIMESTAMPNSSO_TIMESTAMPNS #define SO_MARK36 #define SO_TIMESTAMPING37 #define SCM_TIMESTAMPINGSO_TIMESTAMPING #define SO_PROTOCOL38 #define SO_DOMAIN39 #define SO_RXQ_OVFL40 #endif /* __ASM_GENERIC_SOCKET_H */ |
setsockopt()支持下列选项。其中“类型”表明optval所指数据的类型。
选项 | 类型 | 意义 |
SO_BROADCAST | BOOL | 允许套接口传送广播信息。 |
SO_DEBUG | BOOL | 记录调试信息。 |
SO_DONTLINER | BOOL | 不要因为数据未发送就阻塞关闭操作。设置本选项相当于将SO_LINGER的l_onoff元素置为零。 |
SO_DONTROUTE | BOOL | 禁止选径;直接传送。 |
SO_KEEPALIVE | BOOL | 发送“保持活动”包。 |
SO_LINGER | struct linger FAR* | 如关闭时有未发送数据,则逗留。 |
SO_OOBINLINE | BOOL | 在常规数据流中接收带外数据。 |
SO_RCVBUF | int | 为接收确定缓冲区大小。 |
SO_REUSEADDR | BOOL | 允许套接口和一个已在使用中的地址捆绑(参见bind())。 |
SO_SNDBUF | int | 指定发送缓冲区大小。 |
TCP_NODELAY BOOL | 禁止发送合并的Nagle算法。 |
3.3 属性常用设置
- 发送UDP数据报的时候,设置socket具有广播特性:(默认情况下socket不支持广播特性)
char bBroadcast=1; setsockopt(s,SOL_SOCKET,SO_BROADCAST,(const char*)&bBroadcast,sizeof(char)); |
- 设置socket发送和接收的缓冲区大小。系统默认的状态发送和接收一次为8688字节(约为8.5K);在实际的过程中发送数据和接收数据量比较大,可以设置socket缓冲区。
// 接收缓冲区 int nRecvBuf=20*1024;//设置为20K setsockopt(socketfd,SOL_SOCKET,SO_RCVBUF,(const char*)&nRecvBuf,sizeof(int)); //发送缓冲区 int nSendBuf=20*1024;//设置为20K setsockopt(socketfd,SOL_SOCKET,SO_SNDBUF,(const char*)&nSendBuf,sizeof(int)); |
- 在发送和接收过程中有时由于网络状况等原因,发收不能预期进行,而设置收发时限:
int nNetTimeout=1000;//1秒 //发送时限 setsockopt(socketfd,SOL_SOCKET,SO_SNDTIMEO,(char *)&nNetTimeout,sizeof(int)); //接收时限 setsockopt(socketfd,SOL_SOCKET,SO_RCVTIMEO,(char *)&nNetTimeout,sizeof(int)); |
第四章 Linux内核下TCP/IP参数优化
4.1 查看系统TCP/IP参数的默认值
所有的TCP/IP参数都位于/proc/sys/net目录下。
(请注意,对/proc/sys/net目录下内容的修改都是临时的,任何修改在系统重启后都会丢失)
例如下面这些重要的参数:
参数(路径+文件) | 描述 | 默认值 | 优化值 |
/proc/sys/net/core/rmem_default | 默认的TCP数据接收窗口大小(字节)。 | 229376 | 256960 |
/proc/sys/net/core/rmem_max | 最大的TCP数据接收窗口(字节)。 | 131071 | 513920 |
/proc/sys/net/core/wmem_default | 默认的TCP数据发送窗口大小(字节)。 | 229376 | 256960 |
/proc/sys/net/core/wmem_max | 最大的TCP数据发送窗口(字节)。 | 131071 | 513920 |
/proc/sys/net/core/optmem_max | 表示每个套接字所允许的最大缓冲区的大小。 | 20480 | 81920 |
4.1.2查看TCP接收缓冲区的默认值
[root@wbyq /]# cat /proc/sys/net/ipv4/tcp_rmem 4096873803493888 其中87380表示tcp接收缓冲区的默认值 |
4.1.3 查看TCP发送缓冲区的默认值
[root@wbyq /]# cat /proc/sys/net/ipv4/tcp_wmem
其中16384表示tcp发送缓冲区的默认值 |
4.1.4 tcp 或udp接收缓冲区最大值
[root@wbyq /]# cat /proc/sys/net/core/rmem_max 131071 |
其中131071表示tcp 或 udp 接收缓冲区最大可设置值的一半。
也就是说调用 setsockopt(s, SOL_SOCKET, SO_RCVBUF, &rcv_size, &optlen); 函数时rcv_size大小如果超过 131071,那么getsockopt(s, SOL_SOCKET, SO_RCVBUF, &rcv_size, &optlen); 获取的值就等于 131071 * 2 = 262142
4.1.5 tcp 或udp发送缓冲区最大值
[root@wbyq /]# cat /proc/sys/net/core/wmem_max 131071 |
其中131071表示tcp 或 udp 发送缓冲区最大可设置值的一半。
设置方法与上面接收缓冲区同理。
4.1.6 udp收发缓冲区默认值
[root@wbyq /]# cat /proc/sys/net/core/rmem_default //接收缓冲区默认值 188416 [root@wbyq /]# cat /proc/sys/net/core/wmem_default //发送缓冲区默认值 188416 |
4.1.7 tcp 或udp收发缓冲区最小值
tcp 或udp接收缓冲区的最小值为 256 bytes,由内核的宏决定
tcp 或udp发送缓冲区的最小值为 2048 bytes,由内核的宏决定
4.1.8 TCP重传次数
[root@wbyq /]# cat /proc/sys/net/ipv4/tcp_retries2 15 |
4.2 命令行直接修改系统TCP/IP参数值
- 修改系统套接字缓冲区:
echo 65536 > /proc/sys/net/core/rmem_max
echo 256960 > /proc/sys/net/core/wmem_max
echo 65536 > /proc/sys/net/core/wmen_default
- 修改tcp接收/发送缓冲区:
echo "4096 32768 65536" > /proc/sys/net/ipv4/tcp_rmem
echo "4096 65536 256960" > /proc/sys/net/ipv4/tcp_wmem
- 重传次数:
echo 5 > /proc/sys/net/ipv4/tcp_retries2
4.3 通过代码修改TCP/IP缓冲区大小
系统提供的socket缓冲区大小为8K,你可以将之设置为64K,尤其在传输大文件或者视频文件的时候。
socket发送数据时候先把数据发送到socket缓冲区中,之后接受函数再从缓冲区中取数据,如果发送端特别快的时候,缓冲区很快就被填满(socket默认的是1024×8=8192字节),这时候我们应该根据情况设置缓冲区的大小,可以通过setsockopt函数实现。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <errno.h> #include <sys/types.h> #include <sys/socket.h> #include <assert.h> int main(int argc,char **argv) { int err = -1; /* 返回值 */ int s = -1; /* socket描述符 */ int snd_size = 0; /* 发送缓冲区大小 */ int rcv_size = 0; /* 接收缓冲区大小 */ socklen_t optlen; /* 选项值长度 */ /* * 建立一个TCP套接字 */ s = socket(PF_INET,SOCK_STREAM,0); if( s == -1){ printf("建立套接字错误\n"); return -1; } /* * 先读取缓冲区设置的情况 * 获得原始发送缓冲区大小 */ optlen = sizeof(snd_size); err = getsockopt(s, SOL_SOCKET, SO_SNDBUF,&snd_size, &optlen); if(err<0) { printf("获取发送缓冲区大小错误\n"); } /* * 打印原始缓冲区设置情况 */ /* * 获得原始接收缓冲区大小 */ optlen = sizeof(rcv_size); err = getsockopt(s, SOL_SOCKET, SO_RCVBUF, &rcv_size, &optlen); if(err<0) { printf("获取接收缓冲区大小错误\n"); } printf(" 发送缓冲区原始大小为: %d 字节\n",snd_size); printf(" 接收缓冲区原始大小为: %d 字节\n",rcv_size); /* * 设置发送缓冲区大小 */ snd_size = 10*1024; /* 发送缓冲区大小为8K */ optlen = sizeof(snd_size); err = setsockopt(s, SOL_SOCKET, SO_SNDBUF, &snd_size, optlen); if(err<0) { printf("设置发送缓冲区大小错误\n"); } /* * 设置接收缓冲区大小 */ rcv_size = 10*1024; /* 接收缓冲区大小为8K */ optlen = sizeof(rcv_size); err = setsockopt(s,SOL_SOCKET,SO_RCVBUF, (char *)&rcv_size, optlen); if(err<0) { printf("设置接收缓冲区大小错误\n"); } /* * 检查上述缓冲区设置的情况 * 获得修改后发送缓冲区大小 */ optlen = sizeof(snd_size); err = getsockopt(s, SOL_SOCKET, SO_SNDBUF,&snd_size, &optlen); if(err<0) { printf("获取发送缓冲区大小错误\n"); } /* * 获得修改后接收缓冲区大小 */ optlen = sizeof(rcv_size); err = getsockopt(s, SOL_SOCKET, SO_RCVBUF,(char *)&rcv_size, &optlen); if(err<0) { printf("获取接收缓冲区大小错误\n"); } /* * 打印结果 */ printf(" 发送缓冲区大小为: %d 字节\n",snd_size); printf(" 接收缓冲区大小为: %d 字节\n",rcv_size); close(s); return 0; } |
运行结果分析:
[root@wbyq demo_code2]# ./a.out 发送缓冲区原始大小为: 16384 字节 接收缓冲区原始大小为: 87380 字节 发送缓冲区大小为: 20480 字节 接收缓冲区大小为: 20480 字节 |
设置的接收和发送缓冲区大小为:10*1024=10240字节,实际用getoptsock得到是20480字节,实际加了一倍。
4.4 Socket接收缓冲区分析
Socket接收缓冲区每次接收的数据会使用追加的方式存放在缓冲区里。
比如: 客户端给服务器一次发送了10个字节的数据,服务器每次调用read函数读取一个字节,分10次读取也可以将数据全部读取到。
如果客户端给服务器一次发送了大量数据,服务器来不及接收,只要服务器端的接收缓冲区不溢出,收到的数据都会追加方式存放在接收缓冲区里。
示例代码:
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <stdlib.h> #include <sys/select.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #include <string.h> unsigned char rx_buff; /* TCP服务器创建 */ int main(int argc,char **argv) { int tcp_server_fd; //服务器套接字描述符 int tcp_client_fd; //客户端套接字描述符 struct sockaddr_in tcp_server; struct sockaddr_in tcp_client; socklen_t tcp_client_addrlen=0; int tcp_server_port; //服务器的端口号 //判断传入的参数是否合理 if(argc!=2) { printf("参数格式:./tcp_server <端口号>\n"); return -1; } tcp_server_port=atoi(argv[1]); //将字符串转为整数 /*1. 创建网络套接字*/ tcp_server_fd=socket(AF_INET,SOCK_STREAM,0); if(tcp_server_fd<0) { printf("TCP服务器端套接字创建失败!\n"); return -1; } /*2. 绑定端口号,创建服务器*/ tcp_server.sin_family=AF_INET; //IPV4协议类型 tcp_server.sin_port=htons(tcp_server_port);//端口号赋值,将本地字节序转为网络字节序 tcp_server.sin_addr.s_addr=INADDR_ANY; //将本地IP地址赋值给结构体成员 if(bind(tcp_server_fd,(const struct sockaddr*)&tcp_server,sizeof(struct sockaddr))<0) { printf("TCP服务器端口绑定失败!\n"); return -1; } /*3. 设置监听的客户端数量*/ listen(tcp_server_fd,10); /*4. 等待客户端连接*/ tcp_client_addrlen=sizeof(struct sockaddr); tcp_client_fd=accept(tcp_server_fd,(struct sockaddr *)&tcp_client,&tcp_client_addrlen); if(tcp_client_fd<0) { printf("TCP服务器:等待客户端连接失败!\n"); return -1; } //打印连接的客户端地址信息 printf("已经连接的客户端信息: %s:%d\n",inet_ntoa(tcp_client.sin_addr),ntohs(tcp_client.sin_port)); /*5. 数据通信*/ fd_set readfds; //读事件的文件操作集合 int select_state,rx_cnt; //接收返回值 while(1) { /*5.1 清空文件操作集合*/ FD_ZERO(&readfds); /*5.2 添加要监控的文件描述符*/ FD_SET(tcp_client_fd,&readfds); /*5.3 监控文件描述符*/ select_state=select(tcp_client_fd+1,&readfds,NULL,NULL,NULL); if(select_state>0)//表示有事件产生 { /*5.4 测试指定的文件描述符是否产生了读事件*/ if(FD_ISSET(tcp_client_fd,&readfds)) { /*5.5 读取数据 每次读取一个字节数据*/ rx_cnt=read(tcp_client_fd,&rx_buff,1); printf("%d,%c\n",rx_cnt,rx_buff); if(rx_cnt==0) { printf("对方已经断开连接!\n"); break; } } } else if(select_state<0) //表示产生了错误 { printf("select函数产生异常!\n"); break; } } /*6. 关闭连接*/ close(tcp_client_fd); } |
第五章TCP协议案例代码
5.1 TCP协议实现文件发送
5.1.1 实现基本文件发送功能
设计需求: 实现文件发送基本功能,不考虑发送失败、丢包等情况。目的为了了解学习文件基本发送思路。
- TCP客户端实现文件数据接收
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <string.h> #define PROT 8888 /*客户端创建步骤: 1. 创建网络套接字 2. 连接服务器 */ #pragma pack(1) //以下结构体以一个字节对齐 struct tcpinfo { char name[20]; int file_size; }; //./app 192.168.1.1 int main(int argc,char **argv) { if(argc!=2) { printf("参数传递方式:./app 192.168.1.1\n"); exit(-1); } int client_fd; //存放客户端网络套接字 struct sockaddr_in server_addr; //存放服务器的IP地址信息 /*1. 创建网络套接字 PF_INET:ipv4 、SOCK_STREAM:TCP*/ client_fd=socket(PF_INET,SOCK_STREAM,0); if(client_fd<0) { printf("客户端套接字创建失败!\n"); exit(-1); } /*2. 连接服务器*/ memset(&server_addr,0,sizeof(struct sockaddr_in)); //初始化结构体内存空间 server_addr.sin_family=PF_INET; //ipv4 server_addr.sin_port=htons(PROT); //大端转小端 server_addr.sin_addr.s_addr=inet_addr(argv[1]); //IP地址赋值 if(connect(client_fd,(const struct sockaddr *)&server_addr,sizeof(struct sockaddr))) { printf("连接服务器失败!\n"); exit(-1); } /*编写接收BMP图片代码*/ char buff[1024]; //存放数据缓冲区 int count; //保存读出的字节数 /*接收TCP头指针*/ struct tcpinfo info; while(1) { //2. 读出服务器发送过来的数据 count=read(client_fd,(char*)&info,sizeof(struct tcpinfo)); //判断是否收到数据 if(count==sizeof(struct tcpinfo)) { //4.给服务器发送应答数据 write(client_fd,"1234567890",10); break; } } //1. 创建图片文件 FILE *file=fopen(info.name,"wb"); if(file==NULL) { printf("文件创建失败!\n"); exit(-1); } while(1) { //2. 读出服务器发送过来的数据 count=read(client_fd,buff,1024); if(count>0) { printf("rx count:%d\n",count); //3. 向文件中写数据 fwrite(buff,count,1,file); //4.给服务器发送应答数据 write(client_fd,"1234567890",10); if(count!=1024) { //5. 接收完毕关闭文件 fclose(file); break; } } } printf("文件总大小:%d\n",info.file_size); close(client_fd); //关闭套接字 return 0; } |
- TCP服务端实现文件数据发送
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <string.h> #include <sys/stat.h> #define PROT 8888 #pragma pack(1) //以下结构体以一个字节对齐 struct tcpinfo { char name[20]; int file_size; }; /*服务器端创建步骤: 1. 创建网络套接字 2. 绑定端口 3. 设置监听数量 4. 阻塞等待客户端连接 */ int main(int argc,char **argv) { if(argc!=2) { printf("./app xxx.bmp\n"); exit(-1); } int socket_fd; //存放服务器网络套接字 int client_fd; //存放客户端网络套接字 socklen_t addrlen; //存放接收的客户端地址长度 struct sockaddr_in server_addr; //存放服务器的IP地址信息 struct sockaddr_in client_addr; //存放客户端的IP地址信息 /*1. 创建网络套接字 PF_INET:ipv4 、SOCK_STREAM:TCP*/ socket_fd=socket(PF_INET,SOCK_STREAM,0); if(socket_fd<0) { printf("服务器套接字创建失败!\n"); exit(-1); } /*2. 绑定端口*/ memset(&server_addr,0,sizeof(struct sockaddr_in)); //初始化结构体内存空间 server_addr.sin_family=PF_INET; //ipv4 server_addr.sin_port=htons(PROT); //大端转小端 server_addr.sin_addr.s_addr=INADDR_ANY; //IP地址赋值 if(bind(socket_fd,(const struct sockaddr *)&server_addr,sizeof(struct sockaddr))) { printf("服务器端口绑定失败!\n"); exit(-1); } /*3. 设置监听客户端的数量*/ if(listen(socket_fd,10)) { printf("监听客户端失败!\n"); exit(-1); } /*4. 阻塞等待客户端连接,accept返回客户端的网络套接字*/ client_fd=accept(socket_fd,(struct sockaddr *)&client_addr,&addrlen); if(client_fd<0) { printf("服务器阻塞出现错误!\n"); exit(-1); } /*编写发送图片的代码*/ char buff[1024]; //存放数据缓冲区 char ack_buff[10]; //存放应答数据 int count; //保存读出的字节数 int fd; //文件描述符 struct tcpinfo fileinfo;//存放TCP头结构 struct stat t_stat; //获取文件所有信息的结构体 //1. 打开图片文件 FILE *file=fopen(argv[1],"rb"); if(file==NULL) { printf("打开文件失败!\n"); exit(-1); } /*填充文件*/ //把文件指针转化为文件描述符 fd=fileno(file); /* 获取文件大小 */ fstat(fd, &t_stat); //把打开的文件的状态复制到t_stat结构体中 printf("文件的大小: %d 字节\n",t_stat.st_size); strncpy(fileinfo.name, argv[1],20); fileinfo.file_size=t_stat.st_size; //文件字节大小 /*发送头结构*/ write(client_fd,&fileinfo,sizeof(struct tcpinfo)); while(1) { //接收客户端的应答 count=read(client_fd,ack_buff,10); if(count==10) { break; } } while(1) { //2. 读出图片内容 count=fread(buff,1,1024,file); //3. 向客户端发送数据 count=write(client_fd,buff,count); printf("server tx:%d\n",count); while(1) { //4. 接收客户端的应答 count=read(client_fd,ack_buff,10); if(count==10) { break; } } //5. 判断文件是否到结尾 if(feof(file)) { //5. 关闭文件,结束发送 fclose(file); break; } } close(socket_fd); //关闭套接字 return 0; } |
5.1.2 服务器多线程文件发送
设计需求: 服务器可同时给多个客户端同时传输文件,并且保证文件内容发送准确无误。
(本程序将TCP服务器当做UDP服务器思路设计,展现应答机制)
程序运行流程: 服务器在循环里监听客户端连接,当新的客户端连接上服务器之后,服务器就开一个新的线程,并且对客户端发送一个文件。发送的文件内容进行了封包处理,每一个包数据都包含了文件的详细信息,客户端收到数据包之后,会对数据包进行校验,如果数据包接收错误,客户端将会向服务器回发接收状态报告,接收成功就写入到文件,同时也会服务器回发接收状态报告;如果客户端是第一次接收数据包,就需要先在本地创建一个文件。
如果因为网络问题,客户端没有收到服务器发送的数据包或者客户端向服务器发送的接收状态报告服务器没有接收成功,服务器都会视为发送失败,5s之后会触发重发流程,重复发送上一次的数据包,直到数据包发送完成。
服务器和客户端分别以每秒为单位计算发送和接收的速度,并且增加错误处理,考虑网络不稳定的情况下,数据收发失败的情况。实测可实现远程服务器文件稳定收发。
- 下一版本优化可增加的功能:1. 服务器和客户端分别加上收发时间的记录,为了得到文件收发消耗的时间。
2. 服务器和客户端分别加上Ctrl+C 等一些系统终止的信号捕获,方便在程序终止时,手动释放占用的空间。
本程序使用了多文件编程,相关文件如下:
EncodingPackage.h 、EncodingPackage.c 、tcp_server.c 、 tcp_client.c 、 Makefile - EncodingPackage.h文件代码示例:
#ifndef ENCODEINGPACKAGE_H #define ENCODEINGPACKAGE_H //定义socket文件数据传输的结构体 #pragma pack(1) struct SocketPackageData { unsigned char FrameHead[4]; //帧头固定为0xA1 0xA2 0xA3 0xA4 char FileName[50]; //存放文件名称 char SrcDataBuffer[1024*10];//源数据 unsigned int NumCnt; //记录接收的编号数量 unsigned int FileSize; //文件总大小 unsigned int CurrentSize; //当前文件接收的大小 unsigned int CheckSum; //检验和 }; //定义应答结构,报告文件是否接收成功 struct SocketAckPackageData { unsigned int AckStat; //应答状态 0x80表示接收成功 0x81表示接收失败 }; //数据包的封装与校验 int SetDataPackage(struct SocketPackageData *datapack); int CheckDataPackage(struct SocketPackageData *datapack); #endif |
- EncodingPackage.c文件代码示例:
#include "EncodingPackage.h" /* 函数功能: 封装数据包的数据 函数形参: *datapack :存放数据包结构体的地址 函数返回值: 0表示成功 其他值表示失败 */ int SetDataPackage(struct SocketPackageData *datapack) { /*1. 封装帧头*/ datapack->FrameHead[0]=0xA1; datapack->FrameHead[1]=0xA2; datapack->FrameHead[2]=0xA3; datapack->FrameHead[3]=0xA4; /*2. 计算校验和*/ datapack->CheckSum=0; int i; for(i=0;i<sizeof(datapack->SrcDataBuffer)/sizeof(datapack->SrcDataBuffer[0]);i++) { datapack->CheckSum+=datapack->SrcDataBuffer[i]; } } /* 函数功能: 校验数据包是否正确 函数形参: data :校验的数据包结构 函数返回值: 0表示成功 其他值表示失败 */ int CheckDataPackage(struct SocketPackageData *datapack) { unsigned int checksum=0; int i; /*1. 判断帧头是否正确*/ if(datapack->FrameHead[0]!=0xA1|| datapack->FrameHead[1]!=0xA2|| datapack->FrameHead[2]!=0xA3||datapack->FrameHead[3]!=0xA4) { return -1; } /*2. 判断校验和*/ for(i=0;i<sizeof(datapack->SrcDataBuffer)/sizeof(datapack->SrcDataBuffer[0]);i++) { checksum+=datapack->SrcDataBuffer[i]; } if(checksum!=datapack->CheckSum) { return -1; } return 0; } |
- tcp_server.c文件代码示例:
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <stdlib.h> #include <sys/select.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #include <string.h> #include <pthread.h> #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include "EncodingPackage.h" char SendFileName[1024]; //存放发送的文件名称 /* 函数功能: TCP客户端处理函数 */ void *SocketPthread_func(void *arg) { /*1. 定义相关需要使用的变量*/ unsigned int rx_cnt,rx_cnt2; unsigned int SendOkByte=0; //已经发送成功的字节数 struct SocketPackageData RxTxData; //保存接收和发送的数据 struct SocketAckPackageData AckData; //保存客户端的应答状态 fd_set readfds; //select读事件的文件操作集合 struct timeval timeout; //select超时时间值 int select_state; //接收select返回值 int tcp_client_fd=*(int*)arg; //客户端套接字描述符 free(arg); //释放占用的空间 //计算接收速度 time_t time1; //时间1 time_t time2; //时间2 unsigned int SendByteCnt; //记录上一次的字节数据 char state=1; //状态值 double speed=0.0; //保存速度 /*2. 打开将要发送的文件*/ FILE *fp=fopen(SendFileName,"rb"); if(fp==NULL) { printf("服务器提示:%s文件打开失败!\n",SendFileName); goto ERROR; } /*3. 获取文件的状态信息*/ struct stat FileInfoBuf; if(stat(SendFileName,&FileInfoBuf)) { printf("服务器提示:文件信息获取失败!\n"); goto ERROR; } /*3. 进入到文件发送状态*/ memset(&RxTxData,0,sizeof(struct SocketPackageData));//初始化结构体 RxTxData.FileSize=FileInfoBuf.st_size; //文件总大小 strcpy(RxTxData.FileName,SendFileName); //文件名称 "\123\ 456.c" while(1) { /*4. 文件数据封装发送*/ //读取文件数据 rx_cnt=fread(RxTxData.SrcDataBuffer,1,sizeof(RxTxData.SrcDataBuffer),fp); RxTxData.NumCnt++; //包数量 RxTxData.CurrentSize=rx_cnt; //当前读取的字节数 SetDataPackage(&RxTxData); //结构体数据封包 SendOkByte+=rx_cnt; //记录已经发送的字节数量 printf("服务器发送进度提示: 总大小:%d字节,已发送:%d字节,百分比:%.0f%%\n",RxTxData.FileSize,SendOkByte,SendOkByte/1.0/RxTxData.FileSize*100.0); SEND_SRC_DATA: //触发重发数据的标签 write(tcp_client_fd,&RxTxData,sizeof(struct SocketPackageData)); //发送数据 /*计算接收的速度*/ time1=time(NULL); //获取时间1 if(state) { state=0; time2=time1; //保存时间1 SendByteCnt=SendOkByte; //记录上一次的字节数据 } if(time1-time2>=1) //1秒时间到达 { state=1; speed=(SendOkByte-SendByteCnt)*1.0/1024; //按每秒KB算 } if(speed>1024) //大于1024字节 { printf("实际接收速度:%0.2f mb/s\n",speed*1.0/1024); } else { printf("接收速度:%0.2f kb/s\n",speed); } /*5. 等待客户端的回应*/ WAIT_ACK: //触发继续等待客户端应答 FD_ZERO(&readfds); FD_SET(tcp_client_fd,&readfds); timeout.tv_sec=5; //超时时间 timeout.tv_usec=0; select_state=select(tcp_client_fd+1,&readfds,NULL,NULL,&timeout); if(select_state>0)//表示有事件产生 { //测试指定的文件描述符是否产生了读事件 if(FD_ISSET(tcp_client_fd,&readfds)) { //读取数据 rx_cnt2=read(tcp_client_fd,&AckData,sizeof(struct SocketAckPackageData)); if(rx_cnt2==sizeof(struct SocketAckPackageData)) { if(AckData.AckStat!=0x80) { goto SEND_SRC_DATA;//客户端接收失败,触发重发 } else { //判断文件是否读取完毕 if(rx_cnt!=sizeof(RxTxData.SrcDataBuffer)) { printf("服务器提示:文件发送成功!\n"); break; } } } else if(rx_cnt2>0) { printf("服务器提示:数据包大小接收不正确!\n"); goto WAIT_ACK; //重新等待应答 } if(rx_cnt2==0) { if(SendOkByte==RxTxData.FileSize) { printf("服务器提示:文件发送成功!\n"); } printf("服务器提示:客户端已经断开连接!\n"); break; } } } else if(select_state==0) //接收客户端的应答超时,上一包数据需要重发 { goto SEND_SRC_DATA; } else //表示产生了错误 { printf("服务器提示:select函数产生异常!\n"); break; } } ERROR: /*6. 关闭连接*/ fclose(fp); close(tcp_client_fd); pthread_exit(NULL); } /* TCP服务器创建 */ int main(int argc,char **argv) { int tcp_server_fd; //服务器套接字描述符 int *tcp_client_fd=NULL; //客户端套接字描述符 struct sockaddr_in tcp_server; struct sockaddr_in tcp_client; socklen_t tcp_client_addrlen=0; int tcp_server_port; //服务器的端口号 //判断传入的参数是否合理 if(argc!=3) { printf("参数格式:./tcp_server <端口号> <FileName>\n"); return -1; } strcpy(SendFileName,argv[2]); //存放传入的文件名称 tcp_server_port=atoi(argv[1]); //将字符串转为整数 /*1. 创建网络套接字*/ tcp_server_fd=socket(AF_INET,SOCK_STREAM,0); if(tcp_server_fd<0) { printf("TCP服务器端套接字创建失败!\n"); goto ERROR; } int snd_size = 0; /* 发送缓冲区大小 */ int rcv_size = 0; /* 接收缓冲区大小 */ socklen_t optlen; /* 选项值长度 */ int err = -1; /* 返回值 */ /* * 设置发送缓冲区大小 */ snd_size = 20*1024; /* 发送缓冲区大小为*/ optlen = sizeof(snd_size); err = setsockopt(tcp_server_fd, SOL_SOCKET, SO_SNDBUF, &snd_size, optlen); if(err<0) { printf("服务器提示:设置发送缓冲区大小错误\n"); } /* * 设置接收缓冲区大小 */ rcv_size = 20*1024; /* 接收缓冲区大小*/ optlen = sizeof(rcv_size); err = setsockopt(tcp_server_fd,SOL_SOCKET,SO_RCVBUF, (char *)&rcv_size, optlen); if(err<0) { printf("服务器提示:设置接收缓冲区大小错误\n"); } /*2. 绑定端口号,创建服务器*/ tcp_server.sin_family=AF_INET; //IPV4协议类型 tcp_server.sin_port=htons(tcp_server_port);//端口号赋值,将本地字节序转为网络字节序 tcp_server.sin_addr.s_addr=INADDR_ANY; //将本地IP地址赋值给结构体成员 if(bind(tcp_server_fd,(const struct sockaddr*)&tcp_server,sizeof(struct sockaddr))<0) { printf("TCP服务器端口绑定失败!\n"); goto ERROR; } /*3. 设置监听的客户端数量*/ if(listen(tcp_server_fd,100)) { printf("监听数量设置失败!\n"); goto ERROR; } /*4. 等待客户端连接*/ pthread_t thread_id; while(1) { tcp_client_addrlen=sizeof(struct sockaddr); tcp_client_fd=malloc(sizeof(int)); //申请空间 *tcp_client_fd=accept(tcp_server_fd,(struct sockaddr *)&tcp_client,&tcp_client_addrlen); if(*tcp_client_fd<0) { printf("TCP服务器:等待客户端连接失败!\n"); } else { //打印连接的客户端地址信息 printf("客户端上线: %s:%d\n",inet_ntoa(tcp_client.sin_addr),ntohs(tcp_client.sin_port)); /*1. 创建线程*/ if(pthread_create(&thread_id,NULL,SocketPthread_func,(void*)tcp_client_fd)==0) { /*2. 设置分离属性,让线程结束之后自己释放资源*/ pthread_detach(thread_id); } } } //关闭套接字 ERROR: close(tcp_server_fd); return 0; } |
- tcp_client.c文件代码示例:
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <stdlib.h> #include <sys/select.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #include <string.h> #include <pthread.h> #include "EncodingPackage.h" #include <signal.h> char *rx_p; int Package_Cnt=0; //记录已经接收的数据包 int tcp_client_fd; //客户端套接字描述符 int Server_Port; //服务器端口号 struct sockaddr_in tcp_server; //存放服务器的IP地址信息 int rx_len; fd_set readfds; //读事件的文件操作集合 int select_state,rx_cnt; //接收返回值 struct SocketPackageData RxTxData; //保存接收和发送的数据 struct SocketAckPackageData AckData; //保存客户端的应答状态 FILE *NewFile=NULL; //文件指针 unsigned int SendOkByte=0; //记录已经接收的字节数量 unsigned int rx_size=0; unsigned int all_size=0; //计算接收速度 time_t time1; //时间1 time_t time2; //时间2 unsigned int SendByteCnt; //记录上一次的字节数据 char state=1; //状态值 double speed=0.0; //保存速度 char FileName[1024]; //存放文件的路径 //设置socket缓冲区大小 int snd_size = 0; /* 发送缓冲区大小 */ int rcv_size = 0; /* 接收缓冲区大小 */ socklen_t optlen; /* 选项值长度 */ int err = -1; /* 返回值 */ /* 处理异常信号 */ void sighandler(int a) { printf("触发SIGSEGV信号: 产生段错误! 信号值:%d\n",a); exit(-1); } int main(int argc,char **argv) { //捕获段错误信号 signal(SIGSEGV,sighandler); if(argc!=4) { printf("客户端形参格式:./tcp_client <服务器IP地址> <服务器端口号> <文件存放的目录>\n"); return -1; } Server_Port=atoi(argv[2]); //将字符串的端口号转为整型 /*1. 创建网络套接字*/ tcp_client_fd=socket(AF_INET,SOCK_STREAM,0); if(tcp_client_fd<0) { printf("客户端提示:服务器端套接字创建失败!\n"); goto ERROR; } /* * 设置发送缓冲区大小 */ snd_size = 20*1024; /* 发送缓冲区大小为*/ optlen = sizeof(snd_size); err = setsockopt(tcp_client_fd, SOL_SOCKET, SO_SNDBUF, &snd_size, optlen); if(err<0) { printf("服务器提示:设置发送缓冲区大小错误\n"); } /* * 设置接收缓冲区大小 */ rcv_size = 20*1024; /* 接收缓冲区大小*/ optlen = sizeof(rcv_size); err = setsockopt(tcp_client_fd,SOL_SOCKET,SO_RCVBUF, (char *)&rcv_size, optlen); if(err<0) { printf("服务器提示:设置接收缓冲区大小错误\n"); } /*2. 连接到指定的服务器*/ tcp_server.sin_family=AF_INET; //IPV4协议类型 tcp_server.sin_port=htons(Server_Port);//端口号赋值,将本地字节序转为网络字节序 tcp_server.sin_addr.s_addr=inet_addr(argv[1]); //IP地址赋值 if(connect(tcp_client_fd,(const struct sockaddr*)&tcp_server,sizeof(const struct sockaddr))<0) { printf("客户端提示: 连接服务器失败!\n"); goto ERROR; } rx_p=(char*)&RxTxData; //指针 rx_size=sizeof(struct SocketPackageData); all_size=0; while(1) { /*5.1 清空文件操作集合*/ FD_ZERO(&readfds); /*5.2 添加要监控的文件描述符*/ FD_SET(tcp_client_fd,&readfds); /*5.3 监控文件描述符*/ select_state=select(tcp_client_fd+1,&readfds,NULL,NULL,NULL); if(select_state>0)//表示有事件产生 { /*5.4 测试指定的文件描述符是否产生了读事件*/ if(FD_ISSET(tcp_client_fd,&readfds)) { /*5.5 读取数据*/ rx_cnt=read(tcp_client_fd,rx_p,rx_size); if(rx_cnt>0) //收到不完整的数据 { all_size+=rx_cnt; //记录收到的字节数量 //收到数据包 if(all_size>=sizeof(struct SocketPackageData)) { printf("rx_cnt=%d,all_size=%d\n",rx_cnt,all_size); //出现错误的的处理方法 if(all_size!=sizeof(struct SocketPackageData)) { printf("all_size值超出限制=%d\n",all_size); all_size=0; rx_size=sizeof(struct SocketPackageData); //总大小归位 rx_p=(char*)&RxTxData; //指针归位 AckData.AckStat=0x81; //表示接收失败 printf("all_size的值恢复正常=%d\n",all_size); continue; //结束本次循环 } all_size=0; rx_size=sizeof(struct SocketPackageData); //总大小归位 rx_p=(char*)&RxTxData; //指针归位 /*校验数据包是否正确*/ if(CheckDataPackage(&RxTxData)==0) { //判断之前是否已经接收到相同的一次数据包了,如果接收过就不需要再继续写入到文件 //原因: 可能服务器没有收到客户端发送的应答,触发了数据重发 if(Package_Cnt!=RxTxData.NumCnt) { printf("包编号:%d,有效数据:%d\n",RxTxData.NumCnt,RxTxData.CurrentSize); Package_Cnt=RxTxData.NumCnt; //记录上一次的接收编号 if(RxTxData.NumCnt==1) //表示第一次接收数据包 { printf("第一次接收数据包\n"); strcpy(FileName,argv[3]); //拷贝路径 /123/456.c strcat(FileName,RxTxData.FileName); //文件名称 NewFile=fopen(FileName,"wb"); //创建文件 if(NewFile==NULL) { printf("客户端提示: %s 文件创建失败!\n",FileName); fclose(NewFile); goto ERROR; //退出连接 } if(fwrite(RxTxData.SrcDataBuffer,1,RxTxData.CurrentSize,NewFile)!=RxTxData.CurrentSize) { printf("客户端提示: 第%d包向%s文件写入失败!\n",RxTxData.NumCnt,FileName); fclose(NewFile); goto ERROR; //退出连接 } } else //继续接收数据包 { if(fwrite(RxTxData.SrcDataBuffer,1,RxTxData.CurrentSize,NewFile)!=RxTxData.CurrentSize) { printf("客户端提示: 第%d包向%s文件写入失败!\n",RxTxData.NumCnt,FileName); fclose(NewFile); goto ERROR; //退出连接 } } SendOkByte+=RxTxData.CurrentSize; //记录已经接收的字节数量 printf("客户端接收进度提示: 总大小:%d字节,已接收:%d字节,百分比:%0.0f%%\n",RxTxData.FileSize,SendOkByte,SendOkByte/1.0/RxTxData.FileSize*100.0); } //接收到数据包之后向服务器回发应答信号 AckData.AckStat=0x80; //表示接收成功 if(write(tcp_client_fd,&AckData,sizeof(struct SocketAckPackageData))!=sizeof(struct SocketAckPackageData)) { printf("客户端提示: 向服务器应答失败!"); } //判断数据是否接收完毕 if(RxTxData.CurrentSize!=sizeof(RxTxData.SrcDataBuffer)) { printf("客户端提示:文件接收成功!\n"); break; //退出接收 } } else { AckData.AckStat=0x81; //表示接收失败 if(write(tcp_client_fd,&AckData,sizeof(struct SocketAckPackageData))!=sizeof(struct SocketAckPackageData)) { printf("客户端提示: 向服务器应答失败!"); } printf("客户端提示:校验数据包不正确\n"); } } else { rx_size=sizeof(struct SocketPackageData)-rx_cnt; rx_p+=rx_cnt; //偏移文件指针 } time1=time(NULL); //获取时间1 if(state) { state=0; time2=time1; //保存时间1 SendByteCnt=SendOkByte; //记录上一次的字节数据 } if(time1-time2>=1) //1秒时间到达 { state=1; speed=(SendOkByte-SendByteCnt)*1.0/1024; //按每秒KB算 } if(speed>1024) //大于1024字节 { printf("实际接收速度:%0.2f mb/s\n",speed*1.0/1024); } else { printf("接收速度:%0.2f kb/s\n",speed); } //printf("客户端提示: 应该接收%d字节,实际接收%d字节\n",sizeof(struct SocketPackageData),rx_cnt); //printf("客户端提示:数据大小接收不正确....\n"); } if(rx_cnt==0) { printf("客户端提示:服务器已经断开连接!\n"); fclose(NewFile); break; } } } else if(select_state<0) //表示产生了错误 { printf("客户端提示:select函数产生异常!\n"); break; } } ERROR: /*4. 关闭连接*/ //close(tcp_client_fd); shutdown(tcp_client_fd,SHUT_WR); //TCP半关闭,保证缓冲区内的数据全部写完 return 0; } |
- Makefile文件代码示例:
app: gcc EncodingPackage.c tcp_client.c -o app_c -lpthread gcc EncodingPackage.c tcp_server.c -o app_s -lpthread |
- 运行效果图:
5.1.3 服务器多线程文件发送优化版
思路: 当客户端连接上服务器之后,服务器先给客户端发送一个文件信息结构体(包含文件总大小、文件名称信息),客户端收到文件信息再给服务器回应,服务器收到回应之后进开始发送文件内容,客户端将收到的文件内容存入到文件中,直到文件发送完毕。
说明: 客户端在读取服务器发来的文件结构信息,没有考虑追加读取方式,如果网络带宽不足,会导致文件信息读取失败。造成的原因: 服务器给客户端发1000字节,客户端可能会接收2~3次。
- tcp_client.c代码
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <stdlib.h> #include <sys/select.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #include <string.h> #include <pthread.h> #include <signal.h> #define SEND_BYTE 1024*10 //发送的字节数 //定义socket文件数据传输的结构体 #pragma pack(1) struct SocketPackageData { unsigned char FrameHead; //帧头固定为0xA1 char FileName[50]; //存放文件名称 unsigned int FileSize; //文件总大小 unsigned char Check; //校验值固定为0x88 }; //定义应答结构,报告文件是否接收成功 struct SocketAckPackageData { unsigned int AckStat; //应答状态 0x80表示接收成功 0x81表示接收失败 }; char FileName[1024]; //存放文件的路径 int main(int argc,char **argv) { int Package_Cnt=0; //记录已经接收的数据包 int tcp_client_fd; //客户端套接字描述符 int Server_Port; //服务器端口号 struct sockaddr_in tcp_server; //存放服务器的IP地址信息 int rx_len; fd_set readfds; //读事件的文件操作集合 int select_state,rx_cnt; //接收返回值 struct SocketPackageData RxFileData; //保存接收和发送的数据 struct SocketAckPackageData AckData; //保存客户端的应答状态 FILE *NewFile=NULL; //文件指针 int rx_size=0; int all_size=0; //计算接收速度 time_t time1; //时间1 time_t time2; //时间2 unsigned int SendByteCnt; //记录上一次的字节数据 char state=1; //状态值 double speed=0.0; //保存速度 if(argc!=4) { printf("客户端形参格式:./tcp_client <服务器IP地址> <服务器端口号> <文件存放的目录>\n"); return -1; } Server_Port=atoi(argv[2]); //将字符串的端口号转为整型 unsigned char *SrcDataBuffer=(unsigned char *)malloc(SEND_BYTE); if(SrcDataBuffer==NULL) { printf("服务器提示:缓存空间申请失败!\n"); goto ERROR; } /*1. 创建网络套接字*/ tcp_client_fd=socket(AF_INET,SOCK_STREAM,0); if(tcp_client_fd<0) { printf("客户端提示:服务器端套接字创建失败!\n"); goto ERROR; } int snd_size = 0; /* 发送缓冲区大小 */ int rcv_size = 0; /* 接收缓冲区大小 */ socklen_t optlen; /* 选项值长度 */ int err = -1; /* 返回值 */ /* * 设置发送缓冲区大小 */ snd_size = 40*1024; /* 发送缓冲区大小为*/ optlen = sizeof(snd_size); err = setsockopt(tcp_client_fd, SOL_SOCKET, SO_SNDBUF, &snd_size, optlen); if(err<0) { printf("服务器提示:设置发送缓冲区大小错误\n"); } /* * 设置接收缓冲区大小 */ rcv_size = 40*1024; /* 接收缓冲区大小*/ optlen = sizeof(rcv_size); err = setsockopt(tcp_client_fd,SOL_SOCKET,SO_RCVBUF, (char *)&rcv_size, optlen); if(err<0) { printf("服务器提示:设置接收缓冲区大小错误\n"); } /*2. 连接到指定的服务器*/ tcp_server.sin_family=AF_INET; //IPV4协议类型 tcp_server.sin_port=htons(Server_Port);//端口号赋值,将本地字节序转为网络字节序 tcp_server.sin_addr.s_addr=inet_addr(argv[1]); //IP地址赋值 if(connect(tcp_client_fd,(const struct sockaddr*)&tcp_server,sizeof(const struct sockaddr))<0) { printf("客户端提示: 连接服务器失败!\n"); goto ERROR; } FD_ZERO(&readfds); FD_SET(tcp_client_fd,&readfds); select_state=select(tcp_client_fd+1,&readfds,NULL,NULL,NULL); if(select_state>0)//表示有事件产生 { /*5.4 测试指定的文件描述符是否产生了读事件*/ if(FD_ISSET(tcp_client_fd,&readfds)) { /*5.5 读取数据*/ rx_cnt=read(tcp_client_fd,&RxFileData,sizeof(struct SocketPackageData)); if(rx_cnt!=sizeof(struct SocketPackageData)) { printf("客户端提示: 读取服务器文件信息失败!\n"); goto ERROR; } else { if(RxFileData.FrameHead==0xA1&&RxFileData.Check==0x88) { rx_cnt=write(tcp_client_fd,&AckData,sizeof(struct SocketAckPackageData)); if(rx_cnt!=sizeof(struct SocketAckPackageData)) { printf("客户端提示: 给服务器发送应答失败!\n"); return -1; } } else { printf("rx_cnt=%d\n",rx_cnt); printf("客户端提示: 文件信息读取失败!\n"); return -1; } } } } else { printf("客户端提示: select异常!\n"); return -1; } all_size=0; strcpy(FileName,argv[3]); strcat(FileName,RxFileData.FileName); NewFile=fopen(FileName,"wb"); if(NewFile==NULL) { printf("客户端提示: %s文件创建失败!\n",RxFileData.FileName); return -1; } printf("接收的文件名称:%s\n",RxFileData.FileName); printf("接收的文件大小:%d\n",RxFileData.FileSize); while(1) { /*5.1 清空文件操作集合*/ FD_ZERO(&readfds); /*5.2 添加要监控的文件描述符*/ FD_SET(tcp_client_fd,&readfds); /*5.3 监控文件描述符*/ select_state=select(tcp_client_fd+1,&readfds,NULL,NULL,NULL); if(select_state>0)//表示有事件产生 { /*5.4 测试指定的文件描述符是否产生了读事件*/ if(FD_ISSET(tcp_client_fd,&readfds)) { /*5.5 读取数据*/ rx_cnt=read(tcp_client_fd,SrcDataBuffer,SEND_BYTE); if(rx_cnt>0) //收到不完整的数据 { fwrite(SrcDataBuffer,1,rx_cnt,NewFile); all_size+=rx_cnt; //记录收到的字节数量 printf("客户端接收提示: 总大小:%d字节,已接收:%d字节,百分比:%.0f%%\n",RxFileData.FileSize,all_size,all_size/1.0/RxFileData.FileSize*100.0); time1=time(NULL); //获取时间1 if(state) { state=0; time2=time1; //保存时间1 SendByteCnt=all_size; //记录上一次的字节数据 } if(time1-time2>=1) //1秒时间到达 { state=1; speed=(all_size-SendByteCnt)*1.0/1024; //按每秒KB算 } if(speed>1024) //大于1024字节 { printf("实际接收速度:%0.2f mb/s\n",speed*1.0/1024); } else { printf("接收速度:%0.2f kb/s\n",speed); } } else if(rx_cnt==0) { printf("客户端提示:服务器已经断开连接!\n"); printf("客户端提示:文件接收完毕!\n"); break; } else { printf("客户端提示:read函数返回-1!\n"); } } } else if(select_state<0) //表示产生了错误 { printf("客户端提示:select函数产生异常!\n"); break; } } ERROR: /*4. 关闭连接*/ close(tcp_client_fd); fclose(NewFile); free(SrcDataBuffer); return 0; } |
- tcp_server.c代码示例
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <stdlib.h> #include <sys/select.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #include <string.h> #include <pthread.h> #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #define SEND_BYTE 1024*10 //发送的字节数 //定义socket文件数据传输的结构体 #pragma pack(1) struct SocketPackageData { unsigned char FrameHead; //帧头固定为0xA1 char FileName[50]; //存放文件名称 unsigned int FileSize; //文件总大小 unsigned char Check; //校验值固定为0x88 }; //定义应答结构,报告文件是否接收成功 struct SocketAckPackageData { unsigned int AckStat; //应答状态 0x80表示接收成功 0x81表示接收失败 }; char SendFileName[1024]; //存放发送的文件名称 /* 函数功能: TCP客户端处理函数 */ void *SocketPthread_func(void *arg) { /*1. 定义相关需要使用的变量*/ unsigned int tx_cnt; unsigned int all_sizeof=0; unsigned int SendByte=0; //已经发送的字节数 struct SocketPackageData TxFileInfo; //保存发送给客户端的信息 struct SocketAckPackageData AckData; //保存客户端的应答状态 fd_set readfds; //select读事件的文件操作集合 struct timeval timeout; //select超时时间值 int select_state; //接收select返回值 int tcp_client_fd=*(int*)arg; //客户端套接字描述符 free(arg); //释放占用的空间 //计算接收速度 time_t time1; //时间1 time_t time2; //时间2 unsigned int SendByteCnt; //记录上一次的字节数据 char state=1; //状态值 double speed=0.0; //保存速度 unsigned char *SrcDataBuffer=(unsigned char *)malloc(SEND_BYTE); if(SrcDataBuffer==NULL) { printf("服务器提示:缓存空间申请失败!\n"); goto ERROR; } /*2. 打开将要发送的文件*/ FILE *fp=fopen(SendFileName,"rb"); if(fp==NULL) { printf("服务器提示:%s文件打开失败!\n",SendFileName); goto ERROR; } /*3. 获取文件的状态信息*/ struct stat FileInfoBuf; if(stat(SendFileName,&FileInfoBuf)) { printf("服务器提示:文件信息获取失败!\n"); goto ERROR; } /*3. 给客户端发送文件信息*/ memset(&TxFileInfo,0,sizeof(struct SocketPackageData));//初始化结构体 TxFileInfo.FileSize=FileInfoBuf.st_size; //文件总大小 strcpy(TxFileInfo.FileName,SendFileName); //文件名称 "\123\ 456.c" TxFileInfo.FrameHead=0xA1; //帧头 TxFileInfo.Check=0x88; //校验值 tx_cnt=write(tcp_client_fd,&TxFileInfo,sizeof(struct SocketPackageData)); if(tx_cnt!=sizeof(struct SocketPackageData)) //发送数据 { printf("服务器发送进度提示: 文件信息发送失败!\n"); goto ERROR; } /*4. 等待客户端的回应*/ FD_ZERO(&readfds); FD_SET(tcp_client_fd,&readfds); select_state=select(tcp_client_fd+1,&readfds,NULL,NULL,NULL); if(select_state>0)//表示有事件产生 { tx_cnt=read(tcp_client_fd,&AckData,sizeof(struct SocketAckPackageData)); if(tx_cnt!=sizeof(struct SocketAckPackageData)) { printf("服务器发送进度提示: 等待客户端响应失败!\n"); goto ERROR; } if(tx_cnt==0) { printf("服务器发送进度提示: 客户端断开连接!\n"); goto ERROR; } } else if(select_state<0)//表示产生了错误 { printf("服务器提示:select函数产生异常!断开连接!\n"); goto ERROR; } /*5. 进入到文件发送状态*/ while(1) { /*4. 文件数据封装发送*/ //读取文件数据 tx_cnt=fread(SrcDataBuffer,1,SEND_BYTE,fp); if(write(tcp_client_fd,SrcDataBuffer,tx_cnt)!=tx_cnt) //发送数据 { printf("服务器发送提示: 文件发送失败!\n"); goto ERROR; } SendByte+=tx_cnt; //记录已经发送的字节数量 /*计算接收的速度*/ time1=time(NULL); //获取时间1 if(state) { state=0; time2=time1; //保存时间1 SendByteCnt=SendByte; //记录上一次的字节数据 } if(time1-time2>=1) //1秒时间到达 { state=1; speed=(SendByte-SendByteCnt)*1.0/1024; //按每秒KB算 } if(speed>1024) //大于1024字节 { printf("发送速度:%0.2f mb/s\n",speed*1.0/1024); } else { printf("发送速度:%0.2f kb/s\n",speed); } printf("服务器发送进度提示: 总大小:%d字节,已发送:%d字节,百分比:%.0f%%\n",TxFileInfo.FileSize,SendByte,SendByte/1.0/TxFileInfo.FileSize*100.0); if(tx_cnt!=SEND_BYTE) { printf("服务器提示:文件发送完毕!\n"); break; } /*5. 等待客户端的回应*/ FD_ZERO(&readfds); FD_SET(tcp_client_fd,&readfds); timeout.tv_sec=0; //超时时间 timeout.tv_usec=10; select_state=select(tcp_client_fd+1,&readfds,NULL,NULL,&timeout); if(select_state>0)//表示有事件产生 { printf("客户端断开连接!\n"); break; } else if(select_state<0)//表示产生了错误 { printf("服务器提示:select函数产生异常!断开连接!\n"); break; } } ERROR: /*6. 关闭连接*/ fclose(fp); free(SrcDataBuffer); shutdown(tcp_client_fd,SHUT_WR); //TCP半关闭,保证缓冲区内的数据全部写完 pthread_exit(NULL); } /* TCP服务器创建 */ int main(int argc,char **argv) { int tcp_server_fd; //服务器套接字描述符 int *tcp_client_fd=NULL; //客户端套接字描述符 struct sockaddr_in tcp_server; struct sockaddr_in tcp_client; socklen_t tcp_client_addrlen=0; int tcp_server_port; //服务器的端口号 //判断传入的参数是否合理 if(argc!=3) { printf("参数格式:./tcp_server <端口号> <FileName>\n"); return -1; } strcpy(SendFileName,argv[2]); //存放传入的文件名称 tcp_server_port=atoi(argv[1]); //将字符串转为整数 /*1. 创建网络套接字*/ tcp_server_fd=socket(AF_INET,SOCK_STREAM,0); if(tcp_server_fd<0) { printf("TCP服务器端套接字创建失败!\n"); goto ERROR; } int snd_size = 0; /* 发送缓冲区大小 */ int rcv_size = 0; /* 接收缓冲区大小 */ socklen_t optlen; /* 选项值长度 */ int err = -1; /* 返回值 */ /* * 设置发送缓冲区大小 */ snd_size = 40*1024; /* 发送缓冲区大小为*/ optlen = sizeof(snd_size); err = setsockopt(tcp_server_fd, SOL_SOCKET, SO_SNDBUF, &snd_size, optlen); if(err<0) { printf("服务器提示:设置发送缓冲区大小错误\n"); } /* * 设置接收缓冲区大小 */ rcv_size = 40*1024; /* 接收缓冲区大小*/ optlen = sizeof(rcv_size); err = setsockopt(tcp_server_fd,SOL_SOCKET,SO_RCVBUF, (char *)&rcv_size, optlen); if(err<0) { printf("服务器提示:设置接收缓冲区大小错误\n"); } /*2. 绑定端口号,创建服务器*/ tcp_server.sin_family=AF_INET; //IPV4协议类型 tcp_server.sin_port=htons(tcp_server_port);//端口号赋值,将本地字节序转为网络字节序 tcp_server.sin_addr.s_addr=INADDR_ANY; //将本地IP地址赋值给结构体成员 if(bind(tcp_server_fd,(const struct sockaddr*)&tcp_server,sizeof(struct sockaddr))<0) { printf("TCP服务器端口绑定失败!\n"); goto ERROR; } /*3. 设置监听的客户端数量*/ if(listen(tcp_server_fd,100)) { printf("监听数量设置失败!\n"); goto ERROR; } /*4. 等待客户端连接*/ pthread_t thread_id; while(1) { tcp_client_addrlen=sizeof(struct sockaddr); tcp_client_fd=malloc(sizeof(int)); //申请空间 *tcp_client_fd=accept(tcp_server_fd,(struct sockaddr *)&tcp_client,&tcp_client_addrlen); if(*tcp_client_fd<0) { printf("TCP服务器:等待客户端连接失败!\n"); } else { //打印连接的客户端地址信息 printf("客户端上线: %s:%d\n",inet_ntoa(tcp_client.sin_addr),ntohs(tcp_client.sin_port)); /*1. 创建线程*/ if(pthread_create(&thread_id,NULL,SocketPthread_func,(void*)tcp_client_fd)==0) { /*2. 设置分离属性,让线程结束之后自己释放资源*/ pthread_detach(thread_id); } } } //关闭套接字 ERROR: close(tcp_server_fd); return 0; } |
- 示例
5.2 TCP服务器多线程处理客户端连接
5.2.1 服务器多线程方式处理客户端数据
实现功能: 设置服务器最大可监听100个客户端,当客户端连接之后,就创建一个独立的线程。
当服务器收到客户端发来的数据之后,再将数据回发给客户端,如果客户端断开连接,服务器端就关闭连接,退出线程。
#include <stdio.h> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <arpa/inet.h> //使用大小端转换函数 #include <string.h> #include <sys/select.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #include <pthread.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> //函数声明 void pthread_func(void *dev); //相关变量定义 pthread_t thread_id; //存放线程的标识符 int clientfd; //保存TCP客户端的网络套接字 struct sockaddr_in client_address; //存放客户端的信息 socklen_t address_len; //存放客户端结构体信息的长度 /*服务器端口号定义*/ #define P_host 1314 /*TCP服务器代码*/ int main(int argc,char *argv[]) { int socketfd; struct sockaddr_in server_address; //存放服务器的IP地址信息 memset(&server_address,0,sizeof(struct sockaddr_in)); //初始化内存空间 memset(&client_address,0,sizeof(struct sockaddr_in)); //初始化内存空间 server_address.sin_family=PF_INET; //IPV4协议 server_address.sin_port=htons(P_host); //端口号赋值 server_address.sin_addr.s_addr=INADDR_ANY; //本地IP地址 /*1 .创建套接字*/ socketfd=socket(PF_INET,SOCK_STREAM,0); if(socketfd<0) { printf("服务器网络套接字创建失败!\n"); return -1; } int on = 1; setsockopt(socketfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); /*2. 绑定端口,创建服务器*/ if(bind(socketfd,(const struct sockaddr *)&server_address,sizeof(struct sockaddr))!=0) { printf("服务器绑定端口失败!\n"); return -1; } /*3. 设监听的端口数量*/ if(listen(socketfd,100)!=0) { printf("服务器端口监听失败!\n"); return -1; } int i; while(1) { address_len=sizeof(struct sockaddr); //计算结构体大小 20 /*4. 等待客户端连接*/ if((clientfd=accept(socketfd,(struct sockaddr *)&client_address,&address_len))<0) { printf("等待客户端连接失败!\n"); break; } /*打印一些客户端的信息*/ printf("成功连接的客户端端口号:%d\n",ntohs(client_address.sin_port)); printf("成功连接的客户端IP地址:%s\n",inet_ntoa(client_address.sin_addr)); //创建线程 if(pthread_create(&thread_id,NULL,pthread_func,(int*)(&clientfd))!=0) { printf("线程_%d_创建失败!\n",i); } pthread_detach(thread_id); //设置分离属性,自己回收资源 sleep(1); //保存让线程先运行一段时间。主要本程序传形参是使用&地址方式传输的 } return 0; } void pthread_func(void *dev) { int rx_cnt=0; //接收的数量 int clientfd=*(int*)dev; char buffer[1024]={0}; char tcp_tx_buffer[100]; fd_set readfds; struct timeval tv; int stat_data,i; struct stat stat_buf; while(1) { FD_ZERO(&readfds); //清除文件描述符集合 FD_SET(clientfd,&readfds); //设置监听的文件描述符 tv.tv_sec = 0; tv.tv_usec = 10000; /*监控是否有对应的事件发生*/ stat_data=select(clientfd+1,&readfds,NULL,NULL,&tv); if(stat_data>0) //有数据 { stat_data=read(clientfd,buffer,1024); if(stat_data==0)break; //客户端断开连接 rx_cnt++; //记录接收数据的次数 /*向客户端发送数据*/ sprintf(tcp_tx_buffer,"%s(%d)","Server Data Rx ok(xiao long):",rx_cnt); write(clientfd,tcp_tx_buffer,strlen(tcp_tx_buffer)); //向客户端回发数据 write(clientfd,buffer,stat_data); //向客户端回发数据 } if(stat_data<0)break; //出现错误 } close(clientfd); //关闭客户端套接字 } |
5.2.2 物联网IOT云端服务器设计
设计需求: 需要实现每个硬件设备与手机APP之间完成数据通信。
每个硬件设备都有一个全球唯一的96位设备ID号。
每个硬件设备和手机APP都设计为TCP客户端,数据交互通过TCP服务器进行。
网络连接模型图如下:
- 设计服务器代码需要考虑的问题: 每个手机APP或者一个硬件设备如何找到对方,完成通信?解决方法:
手机APP连接服务器之后,每次发送的消息都进行封包处理(结构体),结构体里分别存放需要连接的硬件设备ID号,帧头,发送的源数据,数据校验和等。
硬件设备要与手机APP之间通信也是一样的操作,需要对发送的消息进行封包处理。
服务器在接收到手机APP和硬件设备的数据包之后,根据各自的ID进行匹配,再实现各自的数据转发。
- 数据传输结构体定义示例:
#pragma pack(1) //以下结构体以一个字节对齐 //定义存放客户端传输数据的结构体 struct SocketRxTxData { unsigned char FrameHead[4]; //存放帧头数据, 固定为: 0xA1 0xA2 0xA3 0xA4 unsigned char Databuffer[30]; //存放传输的字符串数据 unsigned int id[3]; //存放96位ID号 unsigned int CheckSum; //存放数据位的校验和 }; |
- 服务器整体程序设计思路
服务器需要设置最大可监听的客户端数量,当有客户端连接到服务器之后,服务器端就创建一个独立线程,并将客户端的socket套接字当做线程形参传入给线程函数。
在客户端运行的线程里,使用select函数监听读事件,如果有数据可读,就读取一包的数据,再进行解包校验。如果收到的数据包是符合要求的,就提取其中的ID值和当前客户端的socket套接字,保存到链表里(每个客户端只需要保存一次),链表是一个全局的结构。
数据保存之后,就遍历链表,查找链表里是否有其他的节点的id与当前客户端的id相同,如果查找成功,就将当前收到的数据包转发给相同id对应的socket套接字,实现数据转发。
如果客户端断开连接,就将它保存在链表里的节点信息删除掉,并退出线程。
- 链表的结构体如下:
#pragma pack(1) //以下结构体以一个字节对齐 //定义存放客户端信息的结构体 struct SocketTcpClient { unsigned int id[3]; //存放96位ID号 int clientfd; //存放客户端文件描述符 struct SocketTcpClient *next; //定义存放下一个地址的成员 }; |
- 服务器完整代码如下:
#include <stdio.h> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <arpa/inet.h> //使用大小端转换函数 #include <string.h> #include <sys/select.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #include <pthread.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <semaphore.h> /*------------------------------------------------------------------------------*/ #pragma pack(1) //以下结构体以一个字节对齐 //定义存放客户端信息的结构体 struct SocketTcpClient { unsigned int id[3]; //存放96位ID号 int clientfd; //存放客户端文件描述符 struct SocketTcpClient *next; //定义存放下一个地址的成员 }; #pragma pack(1) //以下结构体以一个字节对齐 //定义存放客户端传输数据的结构体 struct SocketRxTxData { unsigned char FrameHead[4]; //存放帧头数据, 固定为: 0xA1 0xA2 0xA3 0xA4 unsigned char Databuffer[30]; //存放传输的字符串数据 unsigned int id[3]; //存放96位ID号 unsigned int CheckSum; //存放数据位的校验和 }; //链表处理相关函数声明 void DeleteListNode(struct SocketTcpClient *head,int clientfd); void AddNewListNode(struct SocketTcpClient *head,struct SocketTcpClient NewData); struct SocketTcpClient *CreateListHead(struct SocketTcpClient *head); void DeleteListNode(struct SocketTcpClient *head,int clientfd); struct SocketTcpClient *FindListNode(struct SocketTcpClient *head,int clientfd); struct SocketTcpClient *FindListNode_id(struct SocketTcpClient *head,struct SocketRxTxData data,int clientfd); int GetListNodeCount(struct SocketTcpClient *head); //定义结构体变量 struct SocketTcpClient *TCP_ClientInfoListHead=NULL; //链表头 /*--------------------------------------------------------------------------------*/ int CheckPackageData(struct SocketRxTxData ClientRxTxData); //校验数据是否正确 /*--------------------------------------------------------------------------------*/ //线程函数声明 void *pthread_func(void *dev); /*服务器端口号定义*/ #define P_host 8089 sem_t lock; //信号量结构 /*TCP服务器代码*/ int main(int argc,char *argv[]) { //相关变量定义 pthread_t thread_id; //存放线程的标识符 struct sockaddr_in client_address; //存放客户端的信息 socklen_t address_len; //存放客户端结构体信息的长度 int socketfd; //保存TCP服务器的网络套接字 int *clientfd; //保存TCP客户端的网络套接字 struct sockaddr_in server_address; //存放服务器的IP地址信息 memset(&server_address,0,sizeof(struct sockaddr_in)); //初始化内存空间 memset(&client_address,0,sizeof(struct sockaddr_in)); //初始化内存空间 server_address.sin_family=PF_INET; //IPV4协议 server_address.sin_port=htons(P_host); //端口号赋值 server_address.sin_addr.s_addr=INADDR_ANY; //本地IP地址 /*初始化信号量*/ sem_init(&lock,0,1); //信号量的值初始为 1,表示资源可用 //创建链表头 TCP_ClientInfoListHead=CreateListHead(TCP_ClientInfoListHead); /*1 .创建套接字*/ socketfd=socket(PF_INET,SOCK_STREAM,0); if(socketfd<0) { printf("服务器网络套接字创建失败!\n"); return -1; } int on = 1; setsockopt(socketfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); /*2. 绑定端口,创建服务器*/ if(bind(socketfd,(const struct sockaddr *)&server_address,sizeof(struct sockaddr))!=0) { printf("服务器绑定端口失败!\n"); return -1; } /*3. 设监听的端口数量*/ if(listen(socketfd,100)!=0) { printf("服务器端口监听失败!\n"); return -1; } int i; while(1) { address_len=sizeof(struct sockaddr); //计算结构体大小 20 //申请空间,线程并发执行不能使用变量地址传参数 clientfd=malloc(sizeof(int)); /*4. 等待客户端连接*/ if((*clientfd=accept(socketfd,(struct sockaddr *)&client_address,&address_len))<0) { printf("等待客户端连接失败!\n"); break; } /*打印一些客户端的信息*/ printf("成功连接的客户端端口号:%d\n",ntohs(client_address.sin_port)); printf("成功连接的客户端IP地址:%s\n",inet_ntoa(client_address.sin_addr)); //创建线程 if(pthread_create(&thread_id,NULL,pthread_func,(void*)clientfd)!=0) { printf("线程_%d_创建失败!\n",i); } pthread_detach(thread_id); //设置分离属性,自己回收资源 } sem_destroy(&lock); //注销信号量 return 0; } void *pthread_func(void *dev) { char state=1; //状态位 int clientfd=*(int*)dev; free(dev); //释放dev占用的空间 fd_set readfds; int stat_data,i; struct stat stat_buf; struct SocketTcpClient ClientInfo; //定义存放客户端信息的结构体变量 struct SocketRxTxData ClientRxTxData; //保存接收的数据信息 struct SocketTcpClient *p; //添加到节点 //AddNewListNode(struct SocketTcpClient *head,struct SocketTcpClient NewData); while(1) { FD_ZERO(&readfds); //清除文件描述符集合 FD_SET(clientfd,&readfds); //设置监听的文件描述符 /*监控是否有对应的事件发生*/ stat_data=select(clientfd+1,&readfds,NULL,NULL,NULL); if(stat_data>0) //有数据 { printf("节点数量:%d,%d\n",clientfd,GetListNodeCount(TCP_ClientInfoListHead)); stat_data=read(clientfd,&ClientRxTxData,sizeof(struct SocketRxTxData)); if(stat_data==sizeof(struct SocketRxTxData)) //读取到指定结构体大小的数据 { if(CheckPackageData(ClientRxTxData)==0) //判断数据包是否接收正确 { if(state) { //保存客户端的信息,保存之后添加到链表里去 state=0; ClientInfo.id[0]=ClientRxTxData.id[0]; ClientInfo.id[1]=ClientRxTxData.id[1]; ClientInfo.id[2]=ClientRxTxData.id[2]; ClientInfo.clientfd=clientfd; //保存文件描述符 sem_wait(&lock); //等待信号量 //在链表结尾添加新节点 AddNewListNode(TCP_ClientInfoListHead,ClientInfo); sem_post(&lock); //释放信号量 //printf("server_tx=%d,id1=%d,id2=%d,id3=%d,str=%s\n",clientfd,ClientInfo.id[0],ClientInfo.id[1],ClientInfo.id[2],ClientRxTxData.Databuffer); } //判断对方是否存在 if(p=FindListNode_id(TCP_ClientInfoListHead,ClientRxTxData,clientfd)) { //存在就转发数据 write(p->clientfd,(void*)&ClientRxTxData,sizeof(struct SocketRxTxData)); } else { printf("对方不在线!\n"); //break; //对方不在线,就断开当前客户端连接 } } else { printf("数据包校验错误!\n"); } } else { printf("数据包接收错误!\n"); } if(stat_data==0)break; //客户端断开连接 } if(stat_data<0)break; //出现错误 } sem_wait(&lock); //等待信号量 //将失效的文件描述符从链表中删除掉 DeleteListNode(TCP_ClientInfoListHead,clientfd); sem_post(&lock); //释放信号量 close(clientfd); //关闭客户端套接字 pthread_exit(NULL);//释放线程占用的堆栈空间 } /*---------------------------------------------封包判断数据是否正确------------*/ /* 函数功能:判断传输的数据是否正确 函数返回值: 0正确 其他值错误 */ int CheckPackageData(struct SocketRxTxData ClientRxTxData) { int i; unsigned int CheckSum=0; //存放校验和 /*1. 判断帧头是否正确*/ if(ClientRxTxData.FrameHead[0]!=0xA1|| ClientRxTxData.FrameHead[1]!=0xA2|| ClientRxTxData.FrameHead[2]!=0xA3|| ClientRxTxData.FrameHead[3]!=0xA4) { return -1; } /*2. 计算校验和*/ for(i=0;i<30;i++) { CheckSum+=ClientRxTxData.Databuffer[i]; } if(CheckSum!=ClientRxTxData.CheckSum) //校验失败 { return -1; } return 0; //数据校验成功 } /*------------------------------------------链表处理代码-------------------------------*/ struct SocketTcpClient *CreateListHead(struct SocketTcpClient *head) { if(head==NULL) //判断是否是空链表头 { head=malloc(sizeof(struct SocketTcpClient)); head->next=NULL; //下一个节点为空 } return head; } /* 函数功能:链表结尾添加新的节点 函数参数: head:链表头 NewData:要添加进去的结构体数据 */ void AddNewListNode(struct SocketTcpClient *head,struct SocketTcpClient NewData) { struct SocketTcpClient *p=head; //保存链表头 struct SocketTcpClient *tmp=NULL; //新链表节点 /*1. 找到链表结尾*/ while(p->next!=NULL) { p=p->next; } /*2. 申请新节点*/ tmp=malloc(sizeof(struct SocketTcpClient)); //申请新的节点空间 memcpy(tmp,&NewData,sizeof(struct SocketTcpClient)); //拷贝结构体数据 tmp->next=NULL; //结尾指向空 /*3. 添加新节点到链表结尾*/ p->next=tmp; } /* 函数功能: 根据文件描述符删除指定的链表节点 函数参数: head :链表头 clientfd:文件描述符 */ void DeleteListNode(struct SocketTcpClient *head,int clientfd) { struct SocketTcpClient *p=head; //保存链表头 struct SocketTcpClient *tmp=NULL; //保存链表地址节点 /*1. 查找要删除的链表节点*/ while(p->next!=NULL) { tmp=p; //保存上一个节点的地址 p=p->next; if(p->clientfd==clientfd) //查找到节点 { /*2. 删除节点*/ tmp->next=tmp->next->next; //连接节点 free(p); //释放节点空间 break; } } } /* 函数功能: 根据文件描述符查找指定的链表节点是否存在 函数参数: head :链表头 clientfd:文件描述符 函数返回值: 查找到的节点地址 */ struct SocketTcpClient *FindListNode(struct SocketTcpClient *head,int clientfd) { struct SocketTcpClient *p=head; //保存链表头 /*遍历链表节点*/ while(p->next!=NULL) { p=p->next; if(p->clientfd==clientfd) //查找到节点 { return p; //返回查找到的节点地址 } } return NULL; //查找失败 } /* 函数功能: 根据96位ID查找指定的链表节点是否存在 函数参数: head :链表头 clientfd:文件描述符 函数返回值: 查找到的节点地址 */ struct SocketTcpClient *FindListNode_id(struct SocketTcpClient *head,struct SocketRxTxData data,int clientfd) { struct SocketTcpClient *p=head; //保存链表头 /*遍历链表节点*/ while(p->next!=NULL) { p=p->next; if(p->id[0]==data.id[0]&&p->id[1]==data.id[1]&&p->id[2]==data.id[2]&&p->clientfd!=clientfd) //查找到节点 { return p; //返回查找到的节点地址 } } return NULL; //查找失败 } /* 函数功能: 遍历链表,查找当前节点的数量 */ int GetListNodeCount(struct SocketTcpClient *head) { int cnt=0; struct SocketTcpClient *p=head; /*遍历链表节点*/ while(p->next!=NULL) { p=p->next; cnt++; } return cnt; } |
- 模拟硬件客户端的代码:
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <stdlib.h> #include <arpa/inet.h> #include <sys/select.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #pragma pack(1) //以下结构体以一个字节对齐 //定义存放客户端传输数据的结构体 struct SocketRxTxData { unsigned char FrameHead[4]; //存放帧头数据, 固定为: 0xA1 0xA2 0xA3 0xA4 unsigned char Databuffer[30]; //存放传输的字符串数据 unsigned int id[3]; //存放96位ID号 unsigned int CheckSum; //存放数据位的校验和 }; void SetPackageData(struct SocketRxTxData *p,char *str,int id1,int id2,int id3); int CheckPackageData(struct SocketRxTxData ClientRxTxData); /* TCP客户端创建 */ int main(int argc,char **argv) { struct SocketRxTxData RxTxData; int tcp_client_fd; //客户端套接字描述符 int Server_Port; //服务器端口号 struct sockaddr_in tcp_server; //存放服务器的IP地址信息 int rx_len; struct SocketRxTxData socket_data; struct SocketRxTxData rx_data; fd_set readfds; struct timeval timeout; if(argc!=6) { printf("TCP客户端形参格式:./tcp_client <服务器IP地址> <stringData> <id1> <id2> <id3>\n"); return -1; } Server_Port=1314; //将字符串的端口号转为整型 /*1. 创建网络套接字*/ tcp_client_fd=socket(AF_INET,SOCK_STREAM,0); if(tcp_client_fd<0) { printf("TCP服务器端套接字创建失败!\n"); return -1; } /*2. 连接到指定的服务器*/ tcp_server.sin_family=AF_INET; //IPV4协议类型 tcp_server.sin_port=htons(Server_Port);//端口号赋值,将本地字节序转为网络字节序 tcp_server.sin_addr.s_addr=inet_addr(argv[1]); //IP地址赋值 if(connect(tcp_client_fd,(const struct sockaddr*)&tcp_server,sizeof(const struct sockaddr))<0) { printf("TCP客户端: 连接服务器失败!\n"); return -1; } //封装结构体 SetPackageData(&socket_data,argv[2],atoi(argv[3]),atoi(argv[4]),atoi(argv[5])); while(1) { FD_ZERO(&readfds); //清除文件描述符集合 FD_SET(tcp_client_fd,&readfds); //设置监听的文件描述符 timeout.tv_sec=1; timeout.tv_usec=100; /*监控是否有对应的事件发生*/ rx_len=select(tcp_client_fd+1,&readfds,NULL,NULL,&timeout); if(rx_len>0) //有数据 { rx_len=read(tcp_client_fd,&rx_data,sizeof(struct SocketRxTxData)); if(rx_len==sizeof(struct SocketRxTxData)) { if(CheckPackageData(rx_data)==0) { printf("rx=%s,%d,%d,%d\n",rx_data.Databuffer,rx_data.id[0],rx_data.id[1],rx_data.id[2]); } } if(rx_len==0)break; //客户端断开连接 } if(rx_len<0)break; //出现错误 if(rx_len==0) //没有事件产生,等待超时 { //向服务器发送数据 write(tcp_client_fd,&socket_data,sizeof(struct SocketRxTxData)); } } /*4. 关闭连接*/ close(tcp_client_fd); } /* 函数功能: 数据封包 */ void SetPackageData(struct SocketRxTxData *p,char *str,int id1,int id2,int id3) { int i; p->FrameHead[0]=0xA1; p->FrameHead[1]=0xA2; p->FrameHead[2]=0xA3; p->FrameHead[3]=0xA4; memcpy(p->Databuffer,str,30); p->CheckSum=0; p->id[0]=id1; p->id[1]=id2; p->id[2]=id3; //赋值校验和 for(i=0;i<30;i++) { p->CheckSum+=p->Databuffer[i]; } } /* 函数功能:判断传输的数据是否正确 函数返回值: 0正确 其他值错误 */ int CheckPackageData(struct SocketRxTxData ClientRxTxData) { int i; unsigned int CheckSum=0; //存放校验和 /*1. 判断帧头是否正确*/ if(ClientRxTxData.FrameHead[0]!=0xA1|| ClientRxTxData.FrameHead[1]!=0xA2|| ClientRxTxData.FrameHead[2]!=0xA3|| ClientRxTxData.FrameHead[3]!=0xA4) { return -1; } /*2. 计算校验和*/ for(i=0;i<30;i++) { CheckSum+=ClientRxTxData.Databuffer[i]; } if(CheckSum!=ClientRxTxData.CheckSum) //校验失败 { return -1; } return 0; //数据校验成功 } |
运行效果示例:
- 应用软件演示示例:
5.2.3 模拟局域网群聊天系统
设计需求: 服务器实现用户之间的消息群发。
- 支持对方用户名显示
- 支持用户上线提醒
- 支持显示在线人数
设计思路: 服务器端使用链表结构记录已经登录的服务器,并使用独立的线程处理一个用户的消息,当其中一个用户发送了消息,服务器就将收到的消息转发给链表里已经登录的其他用户。如果当前用户离线,就删除对应的链表节点。
服务器代码设计如下:
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <stdlib.h> #include <sys/select.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #include <string.h> #include <pthread.h> /*--------------------------------*/ //定义socket数据传输的结构体 struct SocketPackageData { unsigned char FrameHead[4]; //帧头固定为0xA1 0xA2 0xA3 0xA4 char Name[50]; //存放名称 char SrcDataBuffer[300]; //源数据 unsigned int CheckSum; //检验和 unsigned int cnt; //记录当前在线人数 unsigned char type; //类型。 0x1表示上线提醒 0x2表示下线提醒 }; //定义服务器端存放客户端信息的结构 struct TCPClientInfo { int clientfd; //客户端的套接字描述符 struct TCPClientInfo *next; //保存下一个节点的地址 }; //定义服务器端存放客户端信息的链表相关函数和变量 struct TCPClientInfo *ClientInfoListHead=NULL; int GetListNodeCnt(struct TCPClientInfo *head); //获取链表的节点数量 void DeleteListNode(struct TCPClientInfo *head,int clientfd); //删除 void AddListNode(struct TCPClientInfo *head,struct TCPClientInfo src_data); //添加 struct TCPClientInfo *CretorListHead(struct TCPClientInfo *head); //创建 //数据包的封装与校验 int SetDataPackage(struct SocketPackageData *datapack,char *name,char *srcdata); int CheckDataPackage(struct SocketPackageData data); /* 函数功能: TCP客户端处理函数 */ void *SocketPthread_func(void *arg) { unsigned int rx_cnt; struct SocketPackageData RxTxData; //保存接收和发送的数据 struct TCPClientInfo *p=ClientInfoListHead; int tcp_client_fd=*(int*)arg; //客户端套接字描述符 free(arg); //释放占用的空间 //添加新连接的客户端套接字到链表中 struct TCPClientInfo src_data; src_data.clientfd=tcp_client_fd; AddListNode(ClientInfoListHead,src_data); //添加 /*5. 数据通信*/ fd_set readfds; //读事件的文件操作集合 int select_state; //接收返回值 int list_cnt=0; while(1) { /*5.1 清空文件操作集合*/ FD_ZERO(&readfds); /*5.2 添加要监控的文件描述符*/ FD_SET(tcp_client_fd,&readfds); /*5.3 监控文件描述符*/ select_state=select(tcp_client_fd+1,&readfds,NULL,NULL,NULL); if(select_state>0)//表示有事件产生 { /*5.4 测试指定的文件描述符是否产生了读事件*/ if(FD_ISSET(tcp_client_fd,&readfds)) { /*5.5 读取数据*/ rx_cnt=read(tcp_client_fd,&RxTxData,sizeof(struct SocketPackageData)); //printf("rx=%d,%s,%s,%X,%X,%X,%X\n",rx_cnt,RxTxData.Name,RxTxData.SrcDataBuffer,RxTxData.FrameHead[0],RxTxData.FrameHead[1],RxTxData.FrameHead[2],RxTxData.FrameHead[3]); if(rx_cnt==sizeof(struct SocketPackageData)) { /*校验数据包是否正确*/ if(CheckDataPackage(RxTxData)==0) { list_cnt=GetListNodeCnt(ClientInfoListHead);//获取链表节点 //printf("在线人数:%d\n",list_cnt); RxTxData.cnt=list_cnt; //当前在线人数 p=ClientInfoListHead; //将指针重新执行链表头 //轮询按照顺序进行转发 while(p->next!=NULL) { p=p->next; if(p->clientfd!=tcp_client_fd) { write(p->clientfd,&RxTxData,sizeof(struct SocketPackageData)); } } } else { printf("数据包校验值不正确!\n"); } } else { printf("数据包大小接收不正确!\n"); } if(rx_cnt==0) { printf("对方已经断开连接!\n"); break; } } } else if(select_state<0) //表示产生了错误 { printf("select函数产生异常!\n"); break; } } //删除已经断开的客户端套接字描述符 DeleteListNode(ClientInfoListHead,tcp_client_fd); //删除 /*6. 关闭连接*/ close(tcp_client_fd); pthread_exit(NULL);//释放线程占用的堆栈空间 } /* TCP服务器创建 */ int main(int argc,char **argv) { int tcp_server_fd; //服务器套接字描述符 int *tcp_client_fd=NULL; //客户端套接字描述符 struct sockaddr_in tcp_server; struct sockaddr_in tcp_client; socklen_t tcp_client_addrlen=0; int tcp_server_port; //服务器的端口号 //判断传入的参数是否合理 if(argc!=2) { printf("参数格式:./tcp_server <端口号>\n"); return -1; } tcp_server_port=atoi(argv[1]); //将字符串转为整数 /*1. 创建网络套接字*/ tcp_server_fd=socket(AF_INET,SOCK_STREAM,0); if(tcp_server_fd<0) { printf("TCP服务器端套接字创建失败!\n"); return -1; } /*2. 绑定端口号,创建服务器*/ tcp_server.sin_family=AF_INET; //IPV4协议类型 tcp_server.sin_port=htons(tcp_server_port);//端口号赋值,将本地字节序转为网络字节序 tcp_server.sin_addr.s_addr=INADDR_ANY; //将本地IP地址赋值给结构体成员 if(bind(tcp_server_fd,(const struct sockaddr*)&tcp_server,sizeof(struct sockaddr))<0) { printf("TCP服务器端口绑定失败!\n"); return -1; } /*3. 设置监听的客户端数量*/ listen(tcp_server_fd,100); /*4. 等待客户端连接*/ pthread_t thread_id; //创建链表头 ClientInfoListHead=CretorListHead(ClientInfoListHead); while(1) { tcp_client_addrlen=sizeof(struct sockaddr); tcp_client_fd=malloc(sizeof(int)); //申请空间 *tcp_client_fd=accept(tcp_server_fd,(struct sockaddr *)&tcp_client,&tcp_client_addrlen); if(*tcp_client_fd<0) { printf("TCP服务器:等待客户端连接失败!\n"); } else { //打印连接的客户端地址信息 printf("已经连接的客户端信息: %s:%d\n",inet_ntoa(tcp_client.sin_addr),ntohs(tcp_client.sin_port)); /*1. 创建线程*/ if(pthread_create(&thread_id,NULL,SocketPthread_func,(void*)tcp_client_fd)==0) { /*2. 设置分离属性,让线程结束之后自己释放资源*/ pthread_detach(thread_id); } } } return 0; } /*----------------------------------------------------------*/ /* 函数功能: 创建链表头 函数形参: 链表头指针 返回值 : 链表头指针 */ struct TCPClientInfo *CretorListHead(struct TCPClientInfo *head) { if(head==NULL) { head=malloc(sizeof(struct TCPClientInfo)); head->next=NULL; //下一个地址为空 } return head; } /* 函数功能: 在链表结尾添加链表节点 函数形参: head :链表头指针 加入的链表节点 */ void AddListNode(struct TCPClientInfo *head,struct TCPClientInfo src_data) { struct TCPClientInfo *p=head; //保存头地址 struct TCPClientInfo *NewNode=NULL; //新节点 /*1. 找到链表结尾的节点*/ while(p->next!=NULL) { p=p->next; //移动到下一个节点 } /*2. 申请新节点*/ NewNode=malloc(sizeof(struct TCPClientInfo)); if(NewNode==NULL) { printf("链表节点空间申请失败!\n"); return; } //内存数据拷贝 memcpy((void*)NewNode,(void*)&src_data,sizeof(struct TCPClientInfo)); NewNode->next=NULL; /*3. 添加链表节点*/ p->next=NewNode; } /* 函数功能: 根据套接字描述符删除指定节点 函数形参: head :链表头指针 clientfd :要删除的文件描述符 */ void DeleteListNode(struct TCPClientInfo *head,int clientfd) { /*1. 寻找套接字描述符对应链表节点*/ struct TCPClientInfo *p=head; struct TCPClientInfo *tmp; //保存上一个节点的地址 while(p->next!=NULL) { tmp=p; //保存节点地址4 p=p->next; //节点5 if(p->clientfd==clientfd) { tmp->next=tmp->next->next; free(p); //释放p指向的节点 } } } /* 函数功能: 获取节点的数量 函数形参: head :链表头指针 函数返回值: 节点的数量 */ int GetListNodeCnt(struct TCPClientInfo *head) { struct TCPClientInfo *p=head; int cnt=0; while(p->next!=NULL) { p=p->next; cnt++; } return cnt; } /* 函数功能: 校验数据包是否正确 函数形参: data :校验的数据包结构 函数返回值: 0表示成功 其他值表示失败 */ int CheckDataPackage(struct SocketPackageData data) { unsigned int checksum=0; int i; /*1. 判断帧头是否正确*/ if(data.FrameHead[0]!=0xA1|| data.FrameHead[1]!=0xA2|| data.FrameHead[2]!=0xA3||data.FrameHead[3]!=0xA4) { printf("帧头校验错误!\n"); return -1; } /*2. 判断校验和*/ //for(i=0;i<sizeof(data.SrcDataBuffer)/sizeof(data.SrcDataBuffer[0]);i++) for(i=0;i<300;i++) { checksum+=data.SrcDataBuffer[i]; } if(checksum!=data.CheckSum) { printf("校验和错误!\n"); return -1; } return 0; } /* 函数功能: 封装数据包的数据 函数形参: *datapack :存放数据包结构体的地址 char *name :用户名称 char *srcdata :源数据字符串 函数返回值: 0表示成功 其他值表示失败 */ int SetDataPackage(struct SocketPackageData *datapack,char *name,char *srcdata) { /*1. 封装帧头*/ datapack->FrameHead[0]=0xA1; datapack->FrameHead[1]=0xA2; datapack->FrameHead[2]=0xA3; datapack->FrameHead[3]=0xA4; /*2. 赋值名称*/ strcpy(datapack->Name,name); strcpy(datapack->SrcDataBuffer,srcdata); /*3. 计算校验和*/ datapack->CheckSum=0; int i; for(i=0;i<300;i++) { datapack->CheckSum+=datapack->SrcDataBuffer[i]; } } |
- 客户端代码设计
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <stdlib.h> #include <sys/select.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #include <string.h> #include <pthread.h> /*--------------------------------*/ //定义socket数据传输的结构体 struct SocketPackageData { unsigned char FrameHead[4]; //帧头固定为0xA1 0xA2 0xA3 0xA4 char Name[50]; //存放名称 char SrcDataBuffer[300]; //源数据 unsigned int CheckSum; //检验和 unsigned int cnt; //记录当前在线人数 unsigned char type; //类型。 0x1表示上线提醒 0x2表示下线提醒 }; //数据包的封装与校验 int SetDataPackage(struct SocketPackageData *datapack,char *name,char *srcdata); int CheckDataPackage(struct SocketPackageData data); int connect_state=0; //表示连接成功 0表示失败 struct SocketPackageData TX_PackageData; void *SocketPthread_func(void*dev) { int tcp_client_fd=*(int*)dev; while(1) { //printf("请输入要发送的数据:"); scanf("%s",TX_PackageData.SrcDataBuffer); TX_PackageData.type=0; SetDataPackage(&TX_PackageData,TX_PackageData.Name,TX_PackageData.SrcDataBuffer); if(connect_state) { write(tcp_client_fd,&TX_PackageData,sizeof(struct SocketPackageData)); } else { pthread_exit(NULL);//释放线程占用的堆栈空间 return NULL; } } } int main(int argc,char **argv) { pthread_t thread_id; int tcp_client_fd; //客户端套接字描述符 int Server_Port; //服务器端口号 struct sockaddr_in tcp_server; //存放服务器的IP地址信息 int rx_len; if(argc!=4) { printf("TCP客户端形参格式:./tcp_client <服务器IP地址> <服务器端口号> <名称>\n"); return -1; } Server_Port=atoi(argv[2]); //将字符串的端口号转为整型 strcpy(TX_PackageData.Name,argv[3]); //赋值名称 /*1. 创建网络套接字*/ tcp_client_fd=socket(AF_INET,SOCK_STREAM,0); if(tcp_client_fd<0) { printf("TCP服务器端套接字创建失败!\n"); return -1; } /*2. 连接到指定的服务器*/ tcp_server.sin_family=AF_INET; //IPV4协议类型 tcp_server.sin_port=htons(Server_Port);//端口号赋值,将本地字节序转为网络字节序 tcp_server.sin_addr.s_addr=inet_addr(argv[1]); //IP地址赋值 if(connect(tcp_client_fd,(const struct sockaddr*)&tcp_server,sizeof(const struct sockaddr))<0) { printf("TCP客户端: 连接服务器失败!\n"); return -1; } connect_state=1 ;//表示连接成功 pthread_create(&thread_id,NULL,SocketPthread_func,(void*)&tcp_client_fd); /* 给服务器发送上线提醒 */ TX_PackageData.type=0x1; SetDataPackage(&TX_PackageData,TX_PackageData.Name,TX_PackageData.SrcDataBuffer); write(tcp_client_fd,&TX_PackageData,sizeof(struct SocketPackageData)); fd_set readfds; //读事件的文件操作集合 int select_state,rx_cnt; //接收返回值 struct SocketPackageData RxTxData; //保存接收和发送的数据 while(1) { /*5.1 清空文件操作集合*/ FD_ZERO(&readfds); /*5.2 添加要监控的文件描述符*/ FD_SET(tcp_client_fd,&readfds); /*5.3 监控文件描述符*/ select_state=select(tcp_client_fd+1,&readfds,NULL,NULL,NULL); if(select_state>0)//表示有事件产生 { /*5.4 测试指定的文件描述符是否产生了读事件*/ if(FD_ISSET(tcp_client_fd,&readfds)) { /*5.5 读取数据*/ rx_cnt=read(tcp_client_fd,&RxTxData,sizeof(struct SocketPackageData)); if(rx_cnt==sizeof(struct SocketPackageData)) { /*校验数据包是否正确*/ if(CheckDataPackage(RxTxData)==0) { if(RxTxData.type==0x1) { printf("%s用户上线,当前在线人数:%d\n",RxTxData.Name,RxTxData.cnt); } else { printf("%s:",RxTxData.Name); printf("%s ",RxTxData.SrcDataBuffer); printf("在线人数:%d\n",RxTxData.cnt); } } else { printf("校验数据包不正确....\n"); } } else { printf("数据大小接收不正确....\n"); } if(rx_cnt==0) { printf("对方已经断开连接!\n"); connect_state=0 ;//表示断开连接 break; } } } else if(select_state<0) //表示产生了错误 { printf("select函数产生异常!\n"); break; } } /*4. 关闭连接*/ close(tcp_client_fd); return 0; } /* 函数功能: 校验数据包是否正确 函数形参: data :校验的数据包结构 函数返回值: 0表示成功 其他值表示失败 */ int CheckDataPackage(struct SocketPackageData data) { unsigned int checksum=0; int i; /*1. 判断帧头是否正确*/ if(data.FrameHead[0]!=0xA1|| data.FrameHead[1]!=0xA2|| data.FrameHead[2]!=0xA3||data.FrameHead[3]!=0xA4) { return -1; } /*2. 判断校验和*/ //for(i=0;i<sizeof(data.SrcDataBuffer)/sizeof(data.SrcDataBuffer[0]);i++) for(i=0;i<300;i++) { checksum+=data.SrcDataBuffer[i]; } if(checksum!=data.CheckSum) { return -1; } return 0; } /* 函数功能: 封装数据包的数据 函数形参: *datapack :存放数据包结构体的地址 char *name :用户名称 char *srcdata :源数据字符串 函数返回值: 0表示成功 其他值表示失败 */ int SetDataPackage(struct SocketPackageData *datapack,char *name,char *srcdata) { /*1. 封装帧头*/ datapack->FrameHead[0]=0xA1; datapack->FrameHead[1]=0xA2; datapack->FrameHead[2]=0xA3; datapack->FrameHead[3]=0xA4; /*2. 赋值名称*/ strcpy(datapack->Name,name); strcpy(datapack->SrcDataBuffer,srcdata); /*3. 计算校验和*/ datapack->CheckSum=0; int i; for(i=0;i<300;i++) { datapack->CheckSum+=datapack->SrcDataBuffer[i]; } } |
- 代码运行效果