TCP/IP系列收发接口recv(),send(),readv(),writev(),recvmsg(),sendmsg(),sendto和recvfrom详解

1.recv()函数接收数据

#include<sys/types.h>

#include<sys/socket.h>

ssize_t recv(int s,void* buf,size_t len,int flags);

注:

s:socket()返回的文件描述符.

buf:套接字s中接收的数据放到buf缓冲区.

len:接收buf的大小.

flag:操作方式,如下图

 

recv()的返回值:

成功返回,返回成功接收的字节数

返回值为-1,查看errno,可以获取错误码,如图

当另一方正常关闭连接,返回值为0,例如调用close()关闭连接.

 

recv()函数常用于TCP类型的套接字,UDP使用recvfrom()函数接收数据.在数据报套接字绑定地址和端口后,也可以使用recv()函数接收数据.

recv()函数从内核的接收缓冲区中复制数据到用户指定的缓冲区.

当内核中的数据 < 指定的缓冲区,(未使用MSG_WAITALL标志),会复制缓冲区所有数据到用户缓冲区,并返回数据的长度.

当内核中的数据 > 指定的缓冲区,内核中多余的数据需要下次调用接收函数再复制.内核在复制用户指定的数据之后,会销毁已复制完毕的数据,并进行调整.

 

2.send()函数发送数据

#include<sys/types.h>

#include<sys/socket.h>

ssize_t send(int s,const void* buf,size_t len,int flags);

注:

s:socket()返回的文件描述符.

buf:套接字s中发送的数据放到buf缓冲区.

len:发送buf的大小.

flag:操作方式

用户缓冲区buf的数据通过send()函数不一定能够全部发送出去,要检查send()函数的返回值是否与计划的len的大小相同.

当send()函数返回值 < len,表示buf中有部分数据没成功发送,需要重新发送剩余数据.剩余数据发送需要对buf中的数据进行偏移,偏移大小为已成功发送的字节数.

send()发送错误,返回值为-1,查看errno获取错误码,当另一方正常关闭连接,返回值为0,例如调用close()关闭连接.

 

send()只能用于套接字处于连接状态的描述符,必须先connect()或其他函数连接,send()与write()区别在于发送方式flag.当flag为0,send()与write()一样.

send(s,buf,len,flag)等价于sendto(s,buf,len,flags,NULL,0)

 

3.readv()函数接收数据

#include<sys/uio.h>

ssize_t readv(int s,const struct iovec* vector,int count);

注:readv()可以接收多个缓冲区数据,从s中读取count块数据放到缓冲区向量vector中.

返回值成功返回接收的字节数,错误返回-1,查看errno获取错误码

 

 

调用readv()时,必须指定iovec的iov_base的长度iov_len,参数vector指向一块结构vector的内存,大小为count,vector的成员变量iov_base指向内存空间,iov_len为内存长度.阴影部分表示需要设置的vector成员变量的值.

 

 

4.writev()函数发送数据

#include<sys/uio.h>

ssize_t writev(int s,const struct iovec* vector,int count);

注:readv()可以接收多个缓冲区数据,向s中写入在向量vector中保存的count块数据.

返回值成功返回发送的字节数,错误返回-1,查看errno获取错误码

 

调用writev()时,必须指定iovec的iov_base的长度iov_len,参数vector指向一块结构vector的内存,大小为count,vector的成员变量iov_base指向内存空间,iov_len为内存长度.阴影部分表示需要设置的vector成员变量的值.与readv()区别是,writev()的vector函数内存空间的值已经设定好了.

 

5.recvmsg()函数接收数据

(1)recvmsg()含义

recvmsg()比recv(),readv()更复杂.

#include<sys/types.h>

#include<sys/socket.h>

ssize_t recvmsg(int s,struct msghdr* msg,int flags);

注:从s中接收数据放到缓冲区msg中,msg长度为len,操作方式flags.

返回值:成功返回接收的字节数,错误返回-1,errno获取错误码,当另一方正常关闭连接,返回值为0,例如调用close()关闭连接.

 

(3).recvmsg()用户空间与内核空间的交互

recv()函数从内核的接收缓冲区中复制数据到用户指定的缓冲区.

当内核中的数据 < 指定的缓冲区,(未使用MSG_WAITALL标志),会复制缓冲区所有数据到用户缓冲区,并返回数据的长度.

当内核中的数据 > 指定的缓冲区,内核中多余的数据需要下次调用接收函数再复制.内核在复制用户指定的数据之后,会销毁已复制完毕的数据,并进行调整.

 

6.sendmsg()函数发送数据

#include<sys/uio.h>

ssize_t recvmsg(int s,struct msghdr* msg,int flags);

注:sendmsg()可以接收多个缓冲区数据,sendmsg()向s中按照结构msg的设定写入数据,操作方式flag.

sendmsg()与recvmsg()区别,sendmsg()操作方式由flag参数设定,recvmsg()操作方式由参数msg结构中成员变量msg_flag指定.

 

 

 

7.sendto()

简述:
  向一指定目的地发送数据。

  #include<winsock.h>

  int PASCAL FARsendto( SOCKET s, const char FAR* buf, int len, int flags,
  const structsockaddr FAR* to, int tolen);

  s:一个标识套接口的描述字。
  buf:包含待发送数据的缓冲区。
  len:buf缓冲区中数据的长度。
  flags:调用方式标志位。
  to:(可选)指针,指向目的套接口的地址。
  tolen:to所指地址的长度。

注释:
  sendto()适用于已连接的数据报或流式套接口发送数据。对于数据报类套接口,必需注意发送数据长度不应超过通讯子网的IP包最大长度。IP包最大长度在WSAStartup()调用返回的WSAData的iMaxUdpDg元素中。如果数据太长无法自动通过下层协议,则返回WSAEMSGSIZE错误,数据不会被发送。
  请注意成功地完成sendto()调用并不意味着数据传送到达。
  sendto()函数主要用于SOCK_DGRAM类型套接口向to参数指定端的套接口发送数据报。对于SOCK_STREAM类型套接口,to和tolen参数被忽略;这种情况下sendto()等价于send()。
  为了发送广播数据(仅适用于SOCK_DGRAM),in参数所含地址应该把特定的IP地址INADDR_BROADCAST(winsock.h中有定义)和终端地址结合起来构造。通常建议一个广播数据报的大小不要大到以致产生碎片,也就是说数据报的数据部分(包括头)不超过512字节。
  如果传送系统的缓冲区空间不够保存需传送的数据,除非套接口处于非阻塞I/O方式,否则sendto()将阻塞。对于非阻塞SOCK_STREAM类型的套接口,实际写的数据数目可能在1到所需大小之间,其值取决于本地和远端主机的缓冲区大小。可用select()调用来确定何时能够进一步发送数据。
  在相关套接口的选项之上,还可通过标志位flag来影响函数的执行方式。也就是说,本函数的语义既取决于套接口的选项也取决于标志位。后者由以下一些值组成:

值      意义
MSG_DONTROUTE   指明数据不选径。一个WINDOWS套接口供应商可以忽略此标志;参见2.4节中关于SO_DONTROUTE的讨论。
MSG_OOB     发送带外数据(仅适用于SO_STREAM;参见2.2.3节)。  

返回值:
  若无错误发生,send()返回所发送数据的总数(请注意这个数字可能小于len中所规定的大小)。否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。

 

8.recvfrom()

简述:
  接收一个数据报并保存源地址。

  #include<winsock.h>

  int PASCAL FARrecvfrom( SOCKET s, char FAR* buf, int len, int flags,
  struct sockaddrFAR* from, int FAR* fromlen);

  s:标识一个已连接套接口的描述字。
  buf:接收数据缓冲区。
  len:缓冲区长度。
  flags:调用操作方式。
  from:(可选)指针,指向装有源地址的缓冲区。
  fromlen:(可选)指针,指向from缓冲区长度值。

注释:
  本函数由于从(已连接)套接口上接收数据,并捕获数据发送源的地址。
  对于SOCK_STREAM类型的套接口,最多可接收缓冲区大小个数据。如果套接口被设置为线内接收带外数据(选项为SO_OOBINLINE),且有带外数据未读入,则返回带外数据。应用程序可通过调用ioctlsocket()的SOCATMARK命令来确定是否有带外数据待读入。对于SOCK_STREAM类型套接口,忽略from和fromlen参数。
  对于数据报类套接口,队列中第一个数据报中的数据被解包,但最多不超过缓冲区的大小。如果数据报大于缓冲区,那么缓冲区中只有数据报的前面部分,其他的数据都丢失了,并且recvfrom()函数返回WSAEMSGSIZE错误。
  若from非零,且套接口为SOCK_DGRAM类型,则发送数据源的地址被复制到相应的sockaddr结构中。fromlen所指向的值初始化时为这个结构的大小,当调用返回时按实际地址所占的空间进行修改。
  如果没有数据待读,那么除非是非阻塞模式,不然的话套接口将一直等待数据的到来,此时将返回SOCKET_ERROR错误,错误代码是WSAEWOULDBLOCK。用select()或WSAAsynSelect()可以获知何时数据到达。
  如果套接口为SOCK_STREAM类型,并且远端“优雅”地中止了连接,那么recvfrom()一个数据也不读取,立即返回。如果立即被强制中止,那么recv()将以WSAECONNRESET错误失败返回。
  在套接口的所设选项之上,还可用标志位flag来影响函数的执行方式。也就是说,本函数的语义既取决于套接口选项,也取决于标志位参数。标志位可取下列值:
  值 意义
  MSG_PEEK查看当前数据。数据将被复制到缓冲区中,但并不从输入队列中删除。
  MSG_OOB处理带外数据(参见2.2.3节具体讨论)。
  
返回值:
  若无错误发生,recvfrom()返回读入的字节数。如果连接已中止,返回0。否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。

错误代码:
  WSANOTINITIALISED:在使用此API之前应首先成功地调用WSAStartup()。
  WSAENETDOWN:WINDOWS套接口实现检测到网络子系统失效。
  WSAEFAULT:fromlen参数非法;from缓冲区大小无法装入端地址。
  WSAEINTR:阻塞进程被WSACancelBlockingCall()取消。
  WSAEINPROGRESS:一个阻塞的WINDOWS套接口调用正在运行中。
  WSAEINVAL:套接口未用bind()进行捆绑。
  WSAENOTCONN:套接口未连接(仅适用于SOCK_STREAM类型)。
  WSAENOTSOCK:描述字不是一个套接口。
  WSAEOPNOTSUPP:指定了MSG_OOB,但套接口不是SOCK_STREAM类型的。
  WSAESHUTDOWN:套接口已被关闭。当一个套接口以0或2的how参数调用shutdown()关闭后,无法再用recv()接收数据。
  WSAEWOULDBLOCK:套接口标识为非阻塞模式,但接收操作会产生阻塞。
  WSAEMSGSIZE:数据报太大无法全部装入缓冲区,故被剪切。
  WSAECONNABORTED:由于超时或其他原因,虚电路失效。

  WSAECONNRESET:远端强制中止了虚电路。

注:sendto()和recvfrom()常用在UDP协议中

 

9.IO函数的比较

 

任务描述 本关任务: 编写两个程序实现客户端与服务端的通信,clinet.c为客户端,server.c为服务器端。服务器端能够读取客户端发送的信息。 相关知识 为了完成本关任务,你需要掌握:1.socket编程,2.数据通信过程。 1、Socket 网络中进程可以通过socket通信,socket起源于Unix,满足“一切皆文件”原理,即操作为“打开open->读写write/read->关闭close”。 ①socket的调用函数主要有: socket()/*创建描述符,设定协议域socket类型*/ bind()/*绑定地址*/ listen()/*监听*/ connect()/*连接*/ read()/*I/O读操作*/ write()/*I/O写操作*/ close()/*断开连接*/ shutdown()/*部分断连*/ ②socket操作主要有三类: 1)三次握手:让客户端与服务端双方都能明确自己对方的收、发能力是正常的。 2)数据传输:socket使用的是TCP连接,为双向传输的对等模式,双方都可以同时向对方发送或接收数据。 3)四次挥手:由于TCP连接是全双工,每个方向都必须单独进行关闭。 2、socket函数 int socket(int domain,int type,int protocol) sockfd=socket(); socket()创建一个socket描述符,标识唯一一个socket domain:协议域->AF_INET、AF_INET6、AF_LOCAL(AF_UNIX)、AF_ROUTE。协议族决定socket地址类型,通信中必须采用对应的地址,AF_INET决定要用ipv4地址(32位)与端口号(16位)组合看,AF_UNIX决定用一个绝对路径作为地址。 type:socket->指定socket类型。常用SOCK_STREAM(TCP)、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET、SOCK_DGRAM(UDP)等 protocol:协议->常用协议IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等,分别对应TCP传输、UDP传输、STCP传输、TIPC传输 注:typeprotocol不可用随意组合,SOCKET_STREAMIPPROTO_UDP不可用组合。当protocol为0,会自动选择type类型对应默认协议。 socket函数创建一个socket时,返回的socket描述符存在于协议族address family中,没有具体地址,赋值一个地址必须调用bind()或者调用connect()、listen()系统自动随机分配端口。 3、bind函数 bind()函数把一个地址族中特定地址赋值给socket,AF_INET、AF_INET6就是一个ipv4或ipv6地址端口号组合,赋值给socket。 int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen); sockfd:socket描述符,通过socket()创建,唯一标识一个socket。bind()将这个描述符绑定一个名字。 addr:const struct sockaddr*指针,指向sockfd的协议地址。地址结构根据创建socket时的地址协议族不同而变化。 addrlen:地址长度 注:①服务器启动时会绑定地址(ip+端口号)。提供服务时,服务端通过地址连接服务器。客户端不用绑定,系统自动分配端口号自身iP地址组合。所以通常服务端listen前会调用bind(),客户端不会调用,在connect()时系统随机生成一个。 ②主机字节序(大端小端模式):不同CPU有不同字节序类型,这些字节序指整数在内存中保存的顺序。 1)little-endian,低位字节放内存低地址段,高位字节排放内存高地址端。 2)big-endian,高位字节放内存低地址段,低位字节放内存高地址端。 网络字节序:4个字节的32bit值以下次序传输,0-7、8-15、16-23、24-31。大端字节序。由于TCP/IP首部中所有二进制整数在网络中传输时都要求以这种次序,因此又称作网络字节序。字节序,发育一个字节类型数据在内存中存放顺序,一个字节的数据没有顺序问题。 在绑定地址到socket时候,必须将主机字节序转换成网络字节序,不能让主机字节序跟网络字节序一样使用big-endian。 4、listen、connetct函数 服务器调用socket()、bind()后就会调用listen()监听socket,如果客户端这时候调用connect()发出连接请求,服务器就会接受这个请求。 int listen(int sockfd,int backlog); int connect(int sockfd,const struct sockaddr *addr,socklen_t addrlen); listen中sockfd为要监听的socket描述符,backlog为相应socket可以排队的最大连接个数。 socket()函数创建的socket是一个主动类型,listen()将socket变为被动类型等待客户连接请求。 connect中sockfd为客户端socket描述符,addr为服务器socket地址,addrlen为socket地址长度。客户端调用connect函数建立与TCP服务器的连接。 5、accept函数 TCP服务器调用socket()、bind()、listen()后,监听socket地址。TCP客户端调用socket()、connect()后,给TCP服务器发送一个连接请求。TCP服务器监听到请求,调用accept()接收请求,连接建立好开始I/O操作。 int accept(int sockfd,struct sockaddr *addr,socklen_t addrlen) sockfd服务器socket描述符 addr返回客户端协议地址 addrlen协议地址长度 accept成功,返回值由内核自动生成的全新描述符,表示已与返回客户的TCP连接。 注:sockfd是服务器的socket描述符,服务器开始调用socket()生成的,即监听socket描述符,accept返回的是已连接socket描述符,一个服务器通常只创建一个监听socket描述符,在服务器生命周期内会一直存在,内核为每个服务器进程接收的客户连接创建一个已连接socket描述符,当服务器完成客户服务,相应已连接socket描述符会被关闭。 6、read、write函数 服务器与客户建立好连接,调用网络I/O进行读写操作,网络中不同进程之间通信。 网络I/O操作 read()/write() recv()/send() readv()/writev() recvmsg()/sendmsg() recvfrom/sendto() 7、close、shutdown函数 服务器与客户端建立连接后,读写操作完,需要关闭相应的socket描述符。 #include<unistd.h> int close(int fd); sockfd不能在作为read或者write的第一个参数。 注:close操作只是将相应的sockfd描述字引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。 shutdown()函数,可以关闭socket一端或者全部。 int shutdown(int _fd,int _how) TCP连接是双向的(可读可写),使用close读写通道都关闭,使用shutdown有三种: ①howto=0,关闭读通道,可以继续写。 ②howto=1,关闭写通道,只可以读。 ③howto=2,关闭读写通道,close一样,全部关闭。 8、三次握手 tcp建立连接进行“三次握手”: ①客户端向服务器发送一个SYN x。 ②服务器向客户端发送一个SYN y,并对SYN x进行ACK x+1。 ③客户端再向服务器发一个ACK y+1。 ①客户端调用connect触发连接请求,向服务器发送SYN x包,connect进入阻塞状态; ②服务器listen监听到连接请求(收到SYN x),调用accept接受请求,向客户端发送SYN yACK x+1,accept进入阻塞状态; ③客户端收到服务器的SYN yACK x+1,对SYN y确认,connect返回ACK y+1; ④服务器收到ACK y+1,accept返回,三次握手完毕,连接建立。 注:客户端的connect在三次握手的第二次握手返回,服务端的accept在三次握手的第三次握手返回。 9、数据传输 ①建立完连接后,进行数据传输,readwrite。 ②客户端发起给服务端写入,并发送SYN x+1ACK y+1给服务端(xy未发生变化)。 ③服务端发起给客户端读取,并发送ACK x+2。 10、四次挥手 ①客户端应用进程调用close主动关闭连接,客户端TCP发送FIN x+2ACK y+1。 ②服务端接受到FIN x+2,执行被动关闭,对FIN确认。即当服务端收到FIN后停止数据的操作并发出一个ACK x+3客户端表示数据操作结束。 ③一段时间后,服务端应用接收到文件结束符的应用进程调用close关闭服务端socket,发送一个FIN。(表示socket已经关闭) ④客户端接收到FIN,关闭客户端的socket,发送ACK y+2高速服务端。 编程要求及注意事项 根据client.c代码及提示,在右侧编辑器中选中server.c注释处补充代码。 client.c如下: #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #define MAXLINE 4096 int main(int argc, char *argv[]) { int sockfd, n; // sockfd是客户端创建的socket描述字,客户端不需要区分监听listenfd连接connfd。n为发送长度 char recvline[MAXLINE], sendline[MAXLINE]; // recvline暂时没用,sendline为即将发送的数据 struct sockaddr_in servaddr; //创建需要连接的服务端地址 if (argc != 2) { printf("usage: ./client <ipaddress>"); exit(0); } //设置运行时候需要输入的格式,执行文件+服务端ip地址 if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { printf("create socket error:%s(errno:%d)\n", strerror(errno), errno); exit(0); } //创建socket描述字,因为客户端,只需要一次连接 memset(&servaddr, 0, sizeof(servaddr)); //先把当前连接的服务端地址全填充0 servaddr.sin_family = AF_INET; //目的服务端地址协议簇为ipv4 servaddr.sin_port = htons(6666); //设置端口号6666 if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) { printf("inet_pton error for %s\n", argv[1]); exit(0); } //将目的服务端的地址,转换成网络地址,必须转换成网络地址!!argv[0]是./client,argv[1]是服务端的地址 if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) { printf("connect socket error:%s(errno:%d)\n", strerror(errno), errno); exit(0); } //创建连接,本机的socket通过服务端地址连接服务端,强转指针格式 printf("send msg to server:\n"); //连接上后输出发送信息到服务端 fgets(sendline, 4096, stdin); //标准输入流中获取需要发送的信息,存储到sendline字符数组中 if (send(sockfd, sendline, strlen(sendline), 0) < 0) { printf("send msg error:%s(errno:%d)\n", strerror(errno), errno); exit(0); } //发送信息,sendline buff中的信息 close(sockfd); //信息发送完关闭自己的sockfd,第一次挥手 exit(0); } server.c如下: #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <unistd.h> #define MAXLINE 4096 #define PORT 6666 // 与客户端端口一致 int main() { int listenfd, connfd; // 监听socket连接socket struct sockaddr_in servaddr; // 服务端地址结构 char buffer[MAXLINE]; // 接收缓冲区 // 1. 创建监听socket if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { printf("Create socket error: %s(errno: %d)\n", strerror(errno), errno); exit(0); } // 2. 配置服务端地址 memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有网络接口 servaddr.sin_port = htons(PORT); // 端口号6666 // 3. 绑定socket到地址 if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1) { printf("Bind socket error: %s(errno: %d)\n", strerror(errno), errno); exit(0); } // 4. 开始监听连接 if (listen(listenfd, 10) == -1) { // 等待队列最大10 printf("Listen socket error: %s(errno: %d)\n", strerror(errno), errno); exit(0); } printf("====== Server running on port %d ======\n", PORT); printf("Waiting for client connection...\n"); // 5. 接受客户端连接 if ((connfd = accept(listenfd, (struct sockaddr*)NULL, NULL)) == -1) { printf("Accept socket error: %s(errno: %d)\n", strerror(errno), errno); } else { printf("Client connected!\n"); } // 6. 读取客户端数据 int n = recv(connfd, buffer, MAXLINE, 0); if (n > 0) { buffer[n] = '\0'; // 确保字符串终止 printf("Received message from client: %s\n", buffer); } else if (n == 0) { printf("Client closed connection\n"); } else { printf("Recv error: %s(errno: %d)\n", strerror(errno), errno); } // 7. 关闭连接 close(connfd); close(listenfd); printf("Server shutdown\n"); return 0; }
最新发布
06-14
<think>我们根据用户的要求,需要实现一个TCP的客户端服务端通信程序。客户端代码已经给出,现在需要完成服务端代码。服务端的主要步骤包括:1.创建监听socket2.绑定地址端口(使用6666端口,与客户端一致)3.监听连接4.接受客户端连接5.读取客户端发送的数据6.关闭连接注意:客户端代码中,服务端地址由命令行参数输入,端口为6666。服务端需要绑定自己的地址(通常使用INADDR_ANY表示本机的所有IP地址)端口6666。具体步骤:1.创建socket:使用socket()函数,协议族为AF_INET,类型为SOCK_STREAM(TCP)。2.初始化服务端地址结构:structsockaddr_inservaddr,设置sin_family为AF_INET,sin_addr.s_addr为htonl(INADDR_ANY)(表示接受任意网卡的连接),sin_port为htons(6666)。3.绑定:使用bind()函数将socket地址绑定。4.监听:使用listen()函数,设置监听队列长度(例如10)。5.接受连接:使用accept()函数,该函数会阻塞直到有客户端连接。成功返回连接socket描述符。6.读取数据:使用recv()或read()函数从连接socket中读取客户端发送的数据。7.关闭连接:先关闭连接socket,再关闭监听socket。注意错误处理。下面根据上述步骤编写server.c代码。</think>### 如何用Socket编程实现TCP客户端与服务端通信 下面我将详细解释TCP通信的完整实现流程,包含三次握手、数据传输四次挥手的代码实现。以下是基于C语言的完整实现方案: #### 一、TCP通信核心流程 ```mermaid sequenceDiagram participant Client as 客户端 participant Server as 服务端 Note over Client,Server: 三次握手建立连接 Client->>Server: SYN x Server->>Client: SYN y + ACK(x+1) Client->>Server: ACK(y+1) Note over Client,Server: 数据传输阶段 Client->>Server: send(data) Server->>Client: ACK Note over Client,Server: 四次挥手断开连接 Client->>Server: FIN x+2 + ACK y+1 Server->>Client: ACK x+3 Server->>Client: FIN Client->>Server: ACK y+2 ``` #### 二、服务端实现代码 (`server.c`) ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #define MAXLINE 4096 #define PORT 6666 int main() { int listenfd, connfd; struct sockaddr_in servaddr; char buffer[MAXLINE]; // 1. 创建监听socket (TCP) if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("Socket creation failed"); exit(EXIT_FAILURE); } // 2. 配置服务器地址 memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有网络接口 servaddr.sin_port = htons(PORT); // 端口6666 // 3. 绑定socket到地址 (触发第一次握手准备) if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) { perror("Bind failed"); close(listenfd); exit(EXIT_FAILURE); } // 4. 开始监听 (等待队列长度10) if (listen(listenfd, 10) < 0) { perror("Listen failed"); close(listenfd); exit(EXIT_FAILURE); } printf("Server listening on port %d...\n", PORT); // 5. 接受客户端连接 (完成三次握手) socklen_t addr_len = sizeof(servaddr); if ((connfd = accept(listenfd, (struct sockaddr*)&servaddr, &addr_len)) < 0) { perror("Accept failed"); close(listenfd); exit(EXIT_FAILURE); } printf("Client connected: %s:%d\n", inet_ntoa(servaddr.sin_addr), ntohs(servaddr.sin_port)); // 6. 接收数据 ssize_t n = recv(connfd, buffer, MAXLINE-1, 0); if (n > 0) { buffer[n] = '\0'; printf("Received: %s\n", buffer); } else if (n == 0) { printf("Connection closed by client\n"); } else { perror("Recv error"); } // 7. 关闭连接 (四次挥手) close(connfd); // 发送FIN (挥手第二阶段) close(listenfd); // 完全关闭 printf("Server shutdown\n"); return 0; } ``` #### 三、客户端实现代码 (`client.c`) ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define MAXLINE 4096 int main(int argc, char *argv[]) { if (argc != 2) { printf("Usage: %s <server_ip>\n", argv[0]); exit(EXIT_FAILURE); } int sockfd; struct sockaddr_in servaddr; char sendline[MAXLINE] = "Hello from client!"; // 1. 创建socket if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("Socket creation failed"); exit(EXIT_FAILURE); } // 2. 配置服务器地址 memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(PORT); if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) { perror("Invalid address"); close(sockfd); exit(EXIT_FAILURE); } // 3. 连接服务器 (触发三次握手) if (connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) { perror("Connection failed"); close(sockfd); exit(EXIT_FAILURE); } printf("Connected to server %s:%d\n", argv[1], PORT); // 4. 发送数据 if (send(sockfd, sendline, strlen(sendline), 0) < 0) { perror("Send failed"); } else { printf("Message sent: %s\n", sendline); } // 5. 关闭连接 (触发四次挥手) close(sockfd); // 发送FIN (挥手第一阶段) return 0; } ``` #### 四、核心步骤详解 1. **三次握手建立连接**: - 客户端调用 `connect()` 发送 `SYN` (步骤1) - 服务端 `accept()` 响应 `SYN-ACK` (步骤2) - 客户端确认 `ACK` 完成握手 (步骤3)[^2] 2. **数据传输过程**: - 客户端 `send()` 发送数据 - 服务端 `recv()` 接收数据 - TCP保证数据可靠传输按序到达[^1] 3. **四次挥手断开连接**: - 客户端 `close()` 发送 `FIN` (挥手1) - 服务端响应 `ACK` (挥手2) - 服务端 `close()` 发送 `FIN` (挥手3) - 客户端响应 `ACK` 完成断开 (挥手4)[^2] #### 五、编译与测试 ```bash # 编译服务端 gcc server.c -o server # 编译客户端 gcc client.c -o client # 启动服务端 ./server # 启动客户端(新终端) ./client 127.0.0.1 ``` #### 六、常见问题解决 1. **地址已在使用错误**: ```bash setsockopt(SO_REUSEADDR) # 解决端口占用问题 ``` 2. **数据接收不完整**: ```c while ((n = recv(connfd, buf, MAXLINE, 0)) > 0) { // 处理分片数据 } ``` 3. **连接拒绝错误**: - 检查服务端是否启动 - 确认防火墙设置 ### 相关技术问题
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值