目录
一、对于Socket、TCP/UDP、端口号的认知:
1.1 什么是Socket:
1.2 TCP/UDP对比:
1.3 端口号的作用:
二、字节序
2.1 字节序相关概念:
2.2 为什么会有字节序:
2.3 主机字节序转换成网络字节序函数原型和头文件:
2.4 网络字节序转换成主机字节序函数原型和头文件:
三、socket服务器和客户端开发步骤
3.1 TCP通信流程:
3.2 UDP通信流程:
四、socket 相关函数
4.1 创建套接字函数socket()原型和头文件:
4.2 绑定套接字函数bind()原型和头文件:
4.3 字符串格式的IP地址转换成网络格式函数inet_aton()原型和头文件:
4.4 网络格式的IP地址转换成字符串格式函数inet_ntoa()原型和头文件:
4.5 监听被绑定的端口函数listen()原型和头文件:
4.6 接收客户端连接请求函数accept()原型和头文件:
4.7 客户端发送连接请求函数connect()原型和头文件:
4.8 TCP发送信息函数send()原型和头文件:
4.9 TCP接收信息函数recv()原型和头文件:
五、实现客户端&服务器通信
5.1 实现客户端和服务器双方聊天:
5.2 实现多个客户端接入服务器通信:
一、对于Socket、TCP/UDP、端口号的认知:
1.1 什么是Socket:
-
所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议栈进行交互的接口。
-
套接字是通信的基石,是支持TCP/IP协议的路通信的基本操作单元。可以将套接字看作不同主机间的进程进行双间通信的端点,它构成了单个主机内及整个网络间的编程界面。套接字存在于通信域中,通信域是为了处理一般的线程通过套接字通信而引进的一种抽象概念。套接字通常和同一个域中的套接字交换数据(数据交换也可能穿越域的界限,但这时一定要执行某种解释程序),各种进程使用这个相同的域互相之间用Internet协议簇来进行通信。
-
Socket(套接字)可以看成是两个网络应用程序进行通信时,各自通信连接中的端点,这是一个逻辑上的概念。它是网络环境中进程间通信的API,也是可以被命名和寻址的通信端点,使用中的每一个套接字都有其类型和一个与之相连进程。通信时其中一个网络应用程序将要传输的一段信息写入它所在主机的 Socket中,该 Socket通过与网络接口卡(NIC)相连的传输介质将这段信息送到另外一台主机的 Socket中,使对方能够接收到这段信息。 Socket是由IP地址和端口结合的,提供向应用层进程传送数据包的机制。
-
Socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。Socket可以看作该模式的一个实现,Socket即是一种特殊的文件,一些Socket函数就是对其进行的操作(读/写IO、打开、关闭)。
-
socket其实就是一根通信电缆两端的电话终端,电话接通后就相当两个socket建立了连接,两个电话之间可以相互通话,两个socket之间就可以实时收发数据,socket仅仅是一个通信工具,通信工具,通信工具重要的事说三遍(OSI模型中的第四层传输层的API接口,这一层通常使用两种协议TCP或UDP来传输)并不是一种协议。TCP、UDP、HTTP才是我们通常理解的协议。
-
也就是说,Socket这个工具一般使用TCP和UDP两种协议来通信,否则光杆socket并没有毛用。其实我们所认识到的互联网中的各种通信:web请求、即时通讯、文件传输和共享等等底层都是通过Socket工具来实现的,所以说互联网一切皆socket。搞懂了socket你就相当于打通了任督二脉。
-
在UNIX、Linux系统中,为了统一对各种硬件的操作,简化接口,不同的硬件设备也都被看成一个文件。对这些文件的操作,等同于对磁盘上普通文件的操作。 为了表示和区分已经打开的文件,UNIX/Linux会为每个文件分配一个ID,这个文件就是一个整数,被称为文件描述符 例如: 通常用 0 来表示标准输入文件(stdin),它对应的硬件设备就是键盘; 通常用 1 来表示标准输出文件(stdout),它对应的硬件设备就是显示器。 网络连接也是一个文件,它也有文件描述符 我们可以通过 socket() 函数来创建一个网络连接,或者说打开一个网络文件,socket() 的返回值就是文件描述符(注意在windows下的socket返回的叫文件句柄,并不是叫文件描述符)。有了文件描述符,我们就可以使用普通的文件操作函数来传输数据了,例如: 用 read() 读取从远程计算机传来的数据; 用 write() 向远程计算机写入数据。
1.2 TCP/UDP对比:
-
TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据前,不需要建立连接。
-
TCP提供可靠的服务,也就是说通过TCP连接传送的数据是无差错,不丢失,不重复且按序到达;UDP是尽最大努力交付,即保证可靠交付。
-
TCP是面向字节流,实际上是TCP把数据看成是一连串无结构的字节流;UDP是面向报文的,UDP没有拥塞控制,因此网络出现拥塞不会是源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议…)。
-
每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信。
-
TCP的首部开销20字节;UDP的首部开销小,只有8个字节。
-
TCP是逻辑通信信道是全双工的可靠信道;UDP是不可靠信道。
1.3 端口号的作用:
一台拥有 IP 地址的主机可以提供许多服务,比如 Web 服务、FTP 服务、 SMTP 服务等。 这些服务完全可以通过 1 个 IP 地址来实现。那么,主机是怎样区分不同的网络服务呢?显然不能只靠 IP 地址,因为 IP 地址与网络服务的关系是一对多的关系。 实际上是通过“ IP 地址 + 端口号”来区分不同的服务的。端口提供了一种访问通道,服务器一般都是通过知名端口号来识别的。例如,对于每个 TCP/IP 实现来说,FTP服务器的 TCP 端口号都是 21 ,每个 Telnet 服务器的 TCP端口号都是 23 ,每个 TFTP( 简单文件传送协议 ) 服务器的 UDP 端口号都是 69 。
端口号,用两个字节表示的整数,它的取值范围是065535。其中0~1023之间的端口号用于一些知名的网络服务和应用,普通的应用程序需要使用1024以上的端口号。如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败。(如果IP地址是相当于一栋楼的楼号的话,那么端口号就相当于是这栋楼里面的房间的房号)
利用 协议 + IP地址 + 端口号 三元组合,就可以标识网络中的进程了,那么进程间的通信就可以利用这个标识与其它进程进行交互。
二、字节序
2.1 字节序相关概念:
-
字节序是指多字节数据在计算机内存中存储或者网络传输时各字节的存储顺序。在设计计算机系统的时候,有两种处理内存中数据的方法:即大端字节序(大端格式)、小端字节序(小端格式)
-
小段字节序(Little endian):将低序字节存储在起始地址
-
大端字节序(Big endian) :将高序字节存储在起始地址
内存地址 | 小段字节序(Little endian) | 大端字节序(Big endian) |
---|---|---|
4000 | 0x04 | 0x01 |
4001 | 0x03 | 0x02 |
4002 | 0x02 | 0x03 |
4003 | 0x01 | 0x04 |
2.2 为什么会有字节序:
-
计算机电路先处理低位字节,效率比较高,因为计算都是从低位开始的。所以,计算机的内部处理都是小端字节序。但是,人类还是习惯读写大端字节序。所以,除了计算机的内部处理,其他的场合几乎都是大端字节序,比如网络传输和文件储存。
-
计算机处理字节序的时候,不知道什么是高位字节,什么是低位字节。它只知道按顺序读取字节,先读第一个字节,再读第二个字节。如果是大端字节序,先读到的就是高位字节,后读到的就是低位字节。小端字节序正好相反。只有读取的时候,才必须区分字节序,其他情况都不用考虑。
2.3 主机字节序转换成网络字节序函数原型和头文件:
#include <arpa/inet.h> // 包含对网络地址的操作函数的头文件
uint16_t htons(uint16_t hostshort); //将16位主机字节序数据转换成网络字节序数据
uint32_t htonl(uint32_t hostlong); //将32位主机字节序数据转换成网络字节序数据
uint16_t 函数返回值,成功返回网络字节序的值
uint16_t hostshort 需要转换的16位主机字节序数据,uint16_t:unsigned short int
uint32_t 函数返回值,成功返回网络字节序的值
uint32_t hostlong 需要转换的32位主机字节序数据,uint32_t:32位无符号整型
2.4 网络字节序转换成主机字节序函数原型和头文件:
#include <arpa/inet.h> // 包含对网络地址的操作函数的头文件
uint16_t ntohs(uint16_t netshort); //将32位网络字节序数据转换成主机字节序数据
uint32_t ntohl(uint32_t netlong); //将16位网络字节序数据转换成主机字节序数据
uint16_t 函数返回值,返回主机字节序的值
uint32_t netlong 需要转换的16位网络字节序数据;uint16_t:unsigned short int
uint32_t 函数返回值,返回主机字节序的值
uint32_t netlong 需要转换的32位网络字节序数据;uint32_t:unsigned int
三、socket服务器和客户端开发步骤
3.1 TCP通信流程:
-
TCP:传输控制协议 (Transmission Control Protocol)。TCP协议是面向连接的通信协议,即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。
-
服务器Server
-
创建套接字(socket)
-
将socket与IP地址和端口绑定(bind)
-
监听被绑定的端口(listen)
-
接收连接请求(accept)
-
从socket中读取客户端发送来的信息(read)
-
向socket中写入信息(write)
-
关闭socket(close)
-
客户端Client
-
创建套接字(socket)
-
连接指定计算机的端口(connect)
-
向socket中写入信息(write)
-
从socket中读取服务端发送过来的消息(read)
-
关闭socket(close)
3.2 UDP通信流程:
用户数据报协议(User Datagram Protocol)。UDP协议是一个面向无连接的协议。传输数据时,不需要建立连接,不管对方端服务是否启动,直接将数据、数据源和目的地都封装在数据包中,直接发送。每个数据包的大小限制在64k以内。它是不可靠协议,因为无连接,所以传输速度快,但是容易丢失数据。日常应用中,例如视频会议、QQ聊天等。
-
服务器Server
-
使用函数socket(),生成套接字文件描述符;
-
通过struct sockaddr_in 结构设置服务器地址和监听端口;
-
使用bind() 函数绑定监听端口,将套接字文件描述符和地址类型变量(struct sockaddr_in )进行绑定;
-
接收客户端的数据,使用recvfrom() 函数接收客户端的网络数据;
-
向客户端发送数据,使用sendto() 函数向服务器主机发送数据;
-
关闭套接字,使用close() 函数释放资源;
-
客户端Client
-
使用socket(),生成套接字文件描述符;
-
通过struct sockaddr_in 结构设置服务器地址和监听端口;
-
向服务器发送数据,sendto() ;
-
接收服务器的数据,recvfrom() ;
-
关闭套接字,close() ;
四、socket 相关函数
4.1 创建套接字函数socket()原型和头文件:
/*
Linux下 man 2 socket查看手册
*/
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
int 函数返回值,成功返回非负套接字描述符,失败返回-1
int domain 指明所使用的协议族,通常为AF_INET,表示互联网协议族(TCP/IP协议族)
1. AF_INET IPv4因特网域
2. AF_INET6 IPv6因特网域
3. AF_UNIX Unix域
4. AF_ROUTE 路由套接字
5. AF_KEY 密钥套接字
6. AF_UNSPEC 未指定
int type 参数设定socket的类型
1. SOCK_STREAM:
流式套接字提供可靠的,面向连接的通信流,它使用TCP协议,从而保证了数据传输的正确性和顺序性
2. SOCK_DGRAM:
数据报套接字定义了一种无连接的服,数据通过相互独立的报文进行传输,是无序的,并且不保证是可靠,无差错的。它使用 数据报协议UDP
3. SOCK_RAW:
允许程序使用底层协议,原始套接字允许对底层协议如IP或ICMP进行直接访问,功能强大但使用较为不便,主要用于一些协议 的开发。
int protocol 通常赋值为“0”,0选择type对应的默认协议
1. IPPROTO_TCP TCP传输协议
2. IPPROTO_UDP UDP传输协议
3. IPPROTO_SCTP SCTP传输协议
4. IPPROTO_TIPC TIPC传输协议
/*函数说明:用于创建套接字,同时指定协议和类型*/
4.2 绑定套接字函数bind()原型和头文件:
/*
Linux下 man 2 bind查看手册
*/
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
int 函数返回值,如果成功则返回0,如果失败则返回-1
int sockfd 是一个socket描述符
struct sockaddr *addr 是一个指向包含有本机IP地址及端口号等信息的sockaddr类型的指针,指向要绑定给sockfd的协议地址结构,这 个地址结构根据地址创建socket时的地址协议族的不同而不同。
struct sockaddr {
sa_family_t sa_family;//协议族
char sa_data[14];//IP+端口号
}
说明:sockaddr在头文件#include <sys/socket.h>中定义,sockaddr的缺陷是:sa_data把目标地址和端口信息混在一起了
struct sockaddr_in {//如何找到这个结构体,在下方有详解
__kernel_sa_family_t sin_family; //协议族
__be16 sin_port; //端口号
struct in_addr sin_addr; //IP地址结构体
unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
sizeof(unsigned short int) - sizeof(struct in_addr)];
/*填充 没有实际意义,只是为跟sockaddr结构在内存在内存中对其,这样两者才能相互*/
};
/* Internet address. */
struct in_addr
{
uint32_t s_addr; /* address in network byte order */
};
说明:sockaddr_in在头文件#include<netinet/in.h>或#include <arpa/inet.h>中定义,该结构体解决了sockaddr的缺陷,把port和addr 分开储存在两个变量中
上述两者结构体长度一样,都是16个字节,即占用的内存大小是一致的,因此可以互相转化。二者是并列结构,指向sockaddr_in结构的指针也可以指向sockaddr。
一般先把sockaddr_in变量赋值后,强制类型转换后传入用sockaddr做参数的函数:sockaddr_in用于socket定义和赋值;sockaddr用于函数参数。
socklen_t addrlen 地址的长度,通常用sizeof(struct sockaddr_in)表示;
/*函数说明:用于绑定IP地址和端口号到 socket*/
4.3 字符串格式的IP地址转换成网络格式函数inet_aton()原型和头文件:
/*
Linux下 man inet_aton查看手册
*/
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);
const char *cp 你的IP地址
struct in_addr *inp 存放你这个IP地址指针结构体(在上面bind()中有这个结构体),例如:&s_addr
/*函数说明:把字符串形式的IP地址如"192.168.1.123"装换为网络能识别的格式*/
4.4 网络格式的IP地址转换成字符串格式函数inet_ntoa()原型和头文件:
/*
Linux下 man inet_ntoa查看手册
*/
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
char *inet_ntoa(struct in_addr inaddr);
struct in_addr inaddr 存放网络格式IP地址的结构体(在上面bind()中有这个结构体)
/*函数说明:把网络格式的IP地址转换成字符串形式*/
4.5 监听被绑定的端口函数listen()原型和头文件:
/*
Linux下 man 2 listen查看手册
*/
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
int 函数返回值,如果成功则返回0,如果失败则返回-1
int sockfd socket系统调用返回的服务端socket描述符
int backlog 指定在请求队列中允许的最大的请求数,大多数系统默认为5
函数功能:
-
设置能处理的最大连接数,listen并未开始接受连线,只是设置了socket的listen模式,listen函数只用于服务器端,服务器进程不知道要与谁进行连接,因此,它不会主动的要求与某个进程连接,只是一直监听是否有其他客户进程与之连接,然后响应该连接请求,并对它做出处理,一个服务进程可以同时处理多个客户进程的连接,主要就连个功能:将一个未连接的套接字转换为一个被动套接字(监听),规定内核为相应套接字排队的最大连接数。
-
内核为任何一个给定监听套接字维护两个队列:
-
未完成连接队列,每个这样的SYN报文段对应其中一项:已由某个客户端发出并到达服务器,而服务器正在等待完成相应的TCP三次握手过程,这些套接字处于SYN_REVD状态
-
已完成连接队列,每个已完成TCP三次握手过程的客户端对应其中一项,这些套接字处于ESTABLISHED状态;
4.6 接收客户端连接请求函数accept()原型和头文件:
/*
Linux下 man 2 accept查看手册
*/
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int 函数返回值,这些系统调用返回被接受套接字的文件描述符(一个非负整数)。如果出现错误,则返回-1
该函数的返回值是一个新的套接字的描述符,返回值是表示已连接的套接字描述符,而第一个参数是服务器监听套接字描述符,一个服务器通常仅仅创建一个监听套接字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建一个已连接套接字(表示TCP三次握手已完成),当服务器完成对某个给定客户的服务时,相应的已连接套接字就会被关闭。
int sockfd 是socket系统调用返回的服务器端socket描述符
struct sockaddr *addr 用来返回已连接的对端(客户端)的协议地址
socklen_t *addrlen 客户端地址长度,注意需要取地址
/*
函数说明:accept函数由TCP服务器调用,用于从已完成连接队列对头返回下一个已完成连接,如果已完成连接队列为空,那么进程被投入睡眠。
*/
4.7 客户端发送连接请求函数connect()原型和头文件:
/*
Linux下 man 2 connect查看手册
*/
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
int 函数返回值,如果连接或者绑定成功则返回0,如果失败则返回-1
int sockfd 客户端创建的socket描述符
struct sockaddr *addr 是服务器端的IP地址和端口号的地址结构指针
socklen_t addrlen 地址的长度,通常被设置为 sizeof(struct sockaddr)
/*函数说明:该函数用于绑定之后的client端(客户端),与服务器建立连接*/
4.8 TCP发送信息函数send()原型和头文件:
/*
Linux下 man 2 send查看手册
*/
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t 函数返回值,如果成功则返回实际发送的字节数,如果失败则返回-1
int sockfd 为已建立好连接的套接字描述符即accept函数的返回值
void *buf 要发送的内容
size_t len 发送内容的长度
int flags 设置为MSG_DONTWAITMSG 时 表示非阻塞,设置为0时 功能和write一样
/*函数说明:函数只能对处于连接状态的套接字进行使用,参数sockfd为已建立好连接的套接字描述符*/
4.9 TCP接收信息函数recv()原型和头文件:
/*
Linux下 man 2 recv查看手册
*/
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t 函数返回值,如果成功则返回实际发送的字节数,失败则返回-1
int sockfd 在哪个套接字接收
void *buf 存放要接收的数据的首地址
size_t len 要接收的数据的字节大小
int flags 设置为MSG_DONTWAITMSG 时 表示非阻塞,设置为0时 功能和read一样
/*函数说明:接收套接字中的数据*/
五、实现客户端&服务器通信
5.1 实现客户端和服务器双方聊天:
/*server1.c*/
#include <stdio.h> // 包含标准输入输出头文件
#include <sys/types.h> // 包含系统数据类型头文件
#include <sys/socket.h> // 包含系统套接字库的头文件
#include <stdlib.h> // 包含标准库头文件
#include <arpa/inet.h> // 包含网络地址转换头文件
#include <netinet/in.h> // 包含IPv4地址头文件
#include <string.h> // 包含字符串头文件
#include <unistd.h> // 包含unistd.h头文件
int main(int argc, char **argv)
{
int s_fd; // 套接字文件描述符
int c_fd; // 客户端套接字文件描述符
int n_read; // 读入字节数
int n_write; // 写出字节数
char readBuf[128] = {0}; // 读入缓冲区
char writeBuf[128] = {0}; // 写出缓冲区
struct sockaddr_in server_addr; // 服务器地址结构体
struct sockaddr_in client_addr; // 客户端地址结构体
memset(&server_addr, 0, sizeof(server_addr)); // 服务器地址结构体清零
memset(&client_addr, 0, sizeof(client_addr)); // 客户端地址结构体清零
if(argc != 3){ // 参数检查
printf("参数错误!请按照格式输入:./server IP地址 端口号\n");
exit(-1);
}
//int socket(int domain, int type, int protocol);
s_fd = socket(AF_INET, SOCK_STREAM, 0); // 创建TCP/IP套接字
if(s_fd == -1){
printf("创建套接字失败!\n");
perror("socket"); // 输出错误信息
exit(-1);
}
server_addr.sin_family = AF_INET; // 设置服务器地址族为IPv4
server_addr.sin_port = htons(atoi(argv[2])); // 设置服务器端口号
//inet_aton("127.0.0.1", &server_addr.sin_addr);
inet_aton(argv[1], &server_addr.sin_addr); // 设置服务器IP地址
//int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
int ret = bind(s_fd,(struct sockaddr *)&server_addr,sizeof(server_addr)); // 绑定服务器地址
if(ret == -1){
printf("绑定服务器地址失败\n");
perror("bind"); // 输出错误信息
exit(-1);
}
//int listen(int sockfd, int backlog);
ret = listen(s_fd, 10); // 监听套接字
if(ret == -1){
printf("监听套接字失败\n");
perror("listen"); // 输出错误信息
exit(-1);
}
printf("服务器启动成功!\n");
int client_addr_len = sizeof(client_addr); // 客户端地址长度
//int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
while(1){
printf("等待客户端连接...\n");
c_fd = accept(s_fd, (struct sockaddr *)&client_addr, &client_addr_len); // 接受客户端连接请求
if(c_fd == -1){
printf("接受客户端连接请求失败\n");
}
printf("客户端连接成功!\n"); // 输出客户端地址
printf("客户端地址:%s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
if(fork() == 0){ // 子进程处理客户端请求
if(fork() == 0){ // 孙子进程处理客户端请求
while(1){
memset(&writeBuf, 0, sizeof(writeBuf)); // 写出缓冲区清零
printf("请输入要发送的数据:\n"); // 输出提示信息
fgets(writeBuf, sizeof(writeBuf), stdin); // 读入用户输入
//ssize_t write(int fd, const void *buf, size_t count);
n_write = write(c_fd, writeBuf, sizeof(writeBuf)); // 写出数据
if(n_write == -1){
printf("发送数据失败\n");
}else{
printf("发送数据:%s,发送字节数:%d\n", writeBuf, n_write); // 输出发送的数据
}
}
}
while(1){
memset(&readBuf, 0, sizeof(readBuf)); // 读入缓冲区清零
//ssize_t read(int fd, void *buf, size_t count);
n_read = read(c_fd, readBuf, sizeof(readBuf)); // 读入数据
if(n_read == -1){
printf("读取数据失败\n");
perror("read"); // 输出错误信息
}else{
printf("接收字节数:%d\n",n_read); // 输出接收到的数据
printf("接收数据:%s\n", readBuf); // 输出接收到的数据
}
}
break; // 子进程退出
}
}
return 0;
}
/*client.c*/
#include <stdio.h> //包含标准输入输出头文件
#include <sys/types.h> //包含系统数据类型头文件
#include <sys/socket.h> //包含套接字头文件
#include <netinet/in.h> //包含IPv4头文件
#include <arpa/inet.h> //包含网络地址转换头文件
#include <stdlib.h> //包含标准库头文件
#include <string.h> //包含字符串头文件
#include <unistd.h> //包含unistd头文件
#include <errno.h> //包含错误号头文件
int main(int argc, char **argv)
{
int c_fd; //客户端套接字文件描述符
int n_write; //写入字节数
int n_read; // 读入字节数
char sendBuf[128] = {0}; //发送数据缓冲区
char readBuf[128]; // 读入数据缓冲区
struct sockaddr_in client_addr; //客户端地址结构体
memset(&client_addr, 0, sizeof(client_addr)); //客户端地址结构体清零
if(argc != 3){ //参数个数不正确
printf("参数错误,请按照格式输入:./client IP地址 端口号\n");
exit(-1);
}
//int socket(int domain, int type, int protocol);
c_fd = socket(AF_INET, SOCK_STREAM, 0); //创建TCP/IP套接字
if(c_fd == -1){
printf("创建套接字失败\n");
perror("socket");
exit(-1);
}
client_addr.sin_family = AF_INET; // 设置客户端地址族为IPv4
client_addr.sin_port = htons(atoi(argv[2])); //设置客户端端口号
inet_aton(argv[1], &client_addr.sin_addr);; //设置客户端IP地址
//int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
int ret = connect(c_fd, (struct sockaddr *)&client_addr, sizeof(struct sockaddr)); //连接服务器
if(ret == -1){
printf("连接服务器失败\n");
perror("connect");
exit(-1);
}
printf("连接服务器成功\n");
while(1){
if(fork() == 0){ //子进程发送数据
while(1){
memset(&sendBuf, 0, sizeof(sendBuf)); //清空发送数据缓冲区
printf("请输入要发送的数据:\n");
fgets(sendBuf, sizeof(sendBuf), stdin); //从控制台获取输入数据
//ssize_t write(int fd, const void *buf, size_t count);
n_write = write(c_fd, sendBuf, sizeof(sendBuf)); //发送数据
if(n_write == -1){
printf("发送数据失败\n");
}else{
printf("发送数据成功,共发送%d字节数据\n", n_write);
printf("发送的数据为:%s\n", sendBuf);
}
}
}
while(1){
memset(&readBuf, 0, sizeof(readBuf)); //清空读入数据缓冲区
// ssize_t read(int fd, void *buf, size_t count);
n_read = read(c_fd, &readBuf, sizeof(readBuf)); //接收数据
if(n_read == -1){
printf("接收数据失败\n");
}else{
printf("接收数据成功,共接收%d字节数据\n", n_read);
printf("接收到的数据为:%s\n", readBuf);
}
}
}
// close(c_fd); //关闭套接字
return 0;
}
5.2 实现多个客户端接入服务器通信:
/*server2.c*/
#include <stdio.h> // 包含标准输入输出头文件
#include <sys/types.h> // 包含系统数据类型头文件
#include <sys/socket.h> // 包含系统套接字库的头文件
#include <stdlib.h> // 包含标准库头文件
#include <arpa/inet.h> // 包含网络地址转换头文件
#include <netinet/in.h> // 包含IPv4地址头文件
#include <string.h> // 包含字符串头文件
#include <unistd.h> // 包含unistd.h头文件
int main(int argc, char **argv)
{
int s_fd; // 套接字文件描述符
int c_fd; // 客户端套接字文件描述符
int n_read; // 读入字节数
int n_write; // 写出字节数
int mark = 0; // 标记
char readBuf[128] = {0}; // 读入缓冲区
char writeBuf[128] = {0}; // 写出缓冲区
struct sockaddr_in server_addr; // 服务器地址结构体
struct sockaddr_in client_addr; // 客户端地址结构体
memset(&server_addr, 0, sizeof(server_addr)); // 服务器地址结构体清零
memset(&client_addr, 0, sizeof(client_addr)); // 客户端地址结构体清零
if(argc != 3){ // 参数检查
printf("参数错误!请按照格式输入:./server IP地址 端口号\n");
exit(-1);
}
//int socket(int domain, int type, int protocol);
s_fd = socket(AF_INET, SOCK_STREAM, 0); // 创建TCP/IP套接字
if(s_fd == -1){
printf("创建套接字失败!\n");
perror("socket"); // 输出错误信息
exit(-1);
}
server_addr.sin_family = AF_INET; // 设置服务器地址族为IPv4
server_addr.sin_port = htons(atoi(argv[2])); // 设置服务器端口号
//inet_aton("127.0.0.1", &server_addr.sin_addr);
inet_aton(argv[1], &server_addr.sin_addr); // 设置服务器IP地址
//int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
int ret = bind(s_fd,(struct sockaddr *)&server_addr,sizeof(server_addr)); // 绑定服务器地址
if(ret == -1){
printf("绑定服务器地址失败\n");
perror("bind"); // 输出错误信息
exit(-1);
}
//int listen(int sockfd, int backlog);
ret = listen(s_fd, 10); // 监听套接字
if(ret == -1){
printf("监听套接字失败\n");
perror("listen"); // 输出错误信息
exit(-1);
}
printf("服务器启动成功!\n");
int client_addr_len = sizeof(client_addr); // 客户端地址长度
//int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
while(1){
printf("等待客户端连接...\n");
c_fd = accept(s_fd, (struct sockaddr *)&client_addr, &client_addr_len); // 接受客户端连接请求
if(c_fd == -1){
printf("接受客户端连接请求失败\n");
}
printf("客户端连接成功!\n"); // 输出客户端地址
printf("客户端地址:%s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
mark++; // 标记
if(fork() == 0){ // 子进程处理客户端请求
if(fork() == 0){ // 孙子进程处理客户端请求
while(1){
sprintf(writeBuf, "Welcome NO.%dclient\n", mark); // 构造数据
//ssize_t write(int fd, const void *buf, size_t count);
n_write = write(c_fd, writeBuf, sizeof(writeBuf)); // 写出数据
if(n_write == -1){
printf("发送数据失败\n");
}else{
printf("发送数据:%s,发送字节数:%d\n", writeBuf, n_write); // 输出发送的数据
}
sleep(3); // 休眠3秒
}
}
while(1){
memset(&readBuf, 0, sizeof(readBuf)); // 读入缓冲区清零
//ssize_t read(int fd, void *buf, size_t count);
n_read = read(c_fd, readBuf, sizeof(readBuf)); // 读入数据
if(n_read == -1){
printf("读取数据失败\n");
perror("read"); // 输出错误信息
}else{
printf("接收字节数:%d\n",n_read); // 输出接收到的数据
printf("接收数据:%s\n", readBuf); // 输出接收到的数据
}
}
break; // 子进程退出
}
}
return 0;
}
/*client.c*/
#include <stdio.h> //包含标准输入输出头文件
#include <sys/types.h> //包含系统数据类型头文件
#include <sys/socket.h> //包含套接字头文件
#include <netinet/in.h> //包含IPv4头文件
#include <arpa/inet.h> //包含网络地址转换头文件
#include <stdlib.h> //包含标准库头文件
#include <string.h> //包含字符串头文件
#include <unistd.h> //包含unistd头文件
#include <errno.h> //包含错误号头文件
int main(int argc, char **argv)
{
int c_fd; //客户端套接字文件描述符
int n_write; //写入字节数
int n_read; // 读入字节数
char sendBuf[128] = {0}; //发送数据缓冲区
char readBuf[128]; // 读入数据缓冲区
struct sockaddr_in client_addr; //客户端地址结构体
memset(&client_addr, 0, sizeof(client_addr)); //客户端地址结构体清零
if(argc != 3){ //参数个数不正确
printf("参数错误,请按照格式输入:./client IP地址 端口号\n");
exit(-1);
}
//int socket(int domain, int type, int protocol);
c_fd = socket(AF_INET, SOCK_STREAM, 0); //创建TCP/IP套接字
if(c_fd == -1){
printf("创建套接字失败\n");
perror("socket");
exit(-1);
}
client_addr.sin_family = AF_INET; // 设置客户端地址族为IPv4
client_addr.sin_port = htons(atoi(argv[2])); //设置客户端端口号
inet_aton(argv[1], &client_addr.sin_addr);; //设置客户端IP地址
//int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
int ret = connect(c_fd, (struct sockaddr *)&client_addr, sizeof(struct sockaddr)); //连接服务器
if(ret == -1){
printf("连接服务器失败\n");
perror("connect");
exit(-1);
}
printf("连接服务器成功\n");
while(1){
if(fork() == 0){ //子进程发送数据
while(1){
memset(&sendBuf, 0, sizeof(sendBuf)); //清空发送数据缓冲区
printf("请输入要发送的数据:\n");
fgets(sendBuf, sizeof(sendBuf), stdin); //从控制台获取输入数据
//ssize_t write(int fd, const void *buf, size_t count);
n_write = write(c_fd, sendBuf, sizeof(sendBuf)); //发送数据
if(n_write == -1){
printf("发送数据失败\n");
}else{
printf("发送数据成功,共发送%d字节数据\n", n_write);
printf("发送的数据为:%s\n", sendBuf);
}
}
}
while(1){
memset(&readBuf, 0, sizeof(readBuf)); //清空读入数据缓冲区
// ssize_t read(int fd, void *buf, size_t count);
n_read = read(c_fd, &readBuf, sizeof(readBuf)); //接收数据
if(n_read == -1){
printf("接收数据失败\n");
}else{
printf("接收数据成功,共接收%d字节数据\n", n_read);
printf("接收到的数据为:%s\n", readBuf);
}
}
}
// close(c_fd); //关闭套接字
return 0;
}