X

TCP、UDP通信总结

一、什么是socket?

Socket的英文原义是“孔”或“插座”。在编程中,Socket被称做套接字,是网络通信中的一种约定。Socket编程的应用无处不在,我们平时用的QQ、微信、浏览器等程序,都与Socket编程有关。我们平时使用浏览器查资料,这个过程的技术原理是怎样的呢?

这里的SYN和ACK是都是标志位,其中SYN代表新建一个连接,ACK代表确认。其中m、n都是随机数。具体说明如:

  • 第一次握手:SYN标志位被置位,客户端向服务端发送一个随机数m。
  • 第二次握手:ACK、SYN标志位被置位。服务端向客户端发送m+1表示确认刚才收到的数据,同时向客户端发送一个随机数n。
  • 第三次握手:ACK标志被置位。客户端向服务端发送n+1表示确认收到数据。

TCP断开(四次挥手)

TCP断开连接时,客户端、服务端要完成四次数据交互工作才能建立连接,常称为四次挥手。可形象比喻为如下对话:

客户端:服务端您好,我发送数据完毕了,即将和您断开连接。

服务端:客户端您好,我稍稍准备一下,再给您断开

服务端:客户端您好,我准备好了,您可以断开连接了。

客户端:好的,合作愉快!

具体示意图为:

(2)UDP通信socket编程过程

(2)Linux的程序:

在linux下,“一切都是文件”,所以这里我们的套接字也当做文件来看待。

服务端程序linux_tcp_server.c:

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

#define BUF_LEN 100

int main(void)
{
int ServerFd, ClientFd;
char Buf[BUF_LEN] = {0};
struct sockaddr ClientAddr;
int addr_len = 0, recv_len = 0;
struct sockaddr_in ServerSockAddr;
int optval = 1;

/* 创建服务端文件描述符 */
if (-1 == (ServerFd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)))
{
printf(“socket error!\n”);
exit(1);
}

/* 设置服务端信息 */

memset(&ServerSockAddr, 0, sizeof(ServerSockAddr)); // 给结构体ServerSockAddr清零
ServerSockAddr.sin_family = AF_INET; // 使用IPv4地址
ServerSockAddr.sin_addr.s_addr = htonl(INADDR_ANY); // 自动获取IP地址
ServerSockAddr.sin_port = htons(6666); // 端口

// 设置地址和端口号可以重复使用
if (setsockopt(ServerFd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0)
{
printf(“setsockopt error!\n”);
exit(1);
}

/* 绑定操作,绑定前加上上面的socket属性可重复使用地址 */
if (-1 == bind(ServerFd, (struct sockaddr*)&ServerSockAddr, sizeof(struct sockaddr)))
{
printf(“bind error!\n”);
exit(1);
}

/* 进入监听状态 */
if (-1 == (listen(ServerFd, 10)))
{
printf(“listen error!\n”);
exit(1);
}

addr_len = sizeof(struct sockaddr);

while (1)
{
/* 监听客户端请求,accept函数返回一个新的套接字,发送和接收都是用这个套接字 */
if (-1 == (ClientFd = accept(ServerFd, (struct sockaddr*)&ClientAddr, &addr_len)))
{
printf(“accept error!\n”);
exit(1);
}

/* 接受客户端的返回数据 */
if ((recv_len = recv(ClientFd, Buf, BUF_LEN, 0)) < 0)
{
printf(“recv error!\n”);
exit(1);
}

printf(“客户端发送过来的数据为:%s\n”, Buf);

/* 发送数据到客户端 */
send(ClientFd, Buf, recv_len, 0);

/* 关闭客户端套接字 */
close(ClientFd);

/* 清空缓冲区 */
memset(Buf, 0, BUF_LEN);
}

return 0;
}

客户端程序linux_tcp_client.c:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_LEN 100

int main(void)
{
int ClientFd;
char Buf[BUF_LEN] = {0};
struct sockaddr_in ServerSockAddr;

/* 向服务器发起请求 */
memset(&ServerSockAddr, 0, sizeof(ServerSockAddr));
ServerSockAddr.sin_family = AF_INET;
ServerSockAddr.sin_addr.s_addr = inet_addr(“127.0.0.1”);
ServerSockAddr.sin_port = htons(6666);

while (1)
{
/* 创建客户端socket */
if (-1 == (ClientFd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)))
{
printf(“socket error!\n”);
exit(1);
}

/* 连接 */
if (-1 == connect(ClientFd, (struct sockaddr*)&ServerSockAddr, sizeof(ServerSockAddr)))
{
printf(“connect error!\n”);
exit(1);
}

printf(“请输入一个字符串,发送给服务端:”);
gets(Buf);
/* 发送数据到服务端 */
send(ClientFd, Buf, strlen(Buf), 0);
memset(Buf, 0, BUF_LEN); // 重置缓冲区

/* 接受服务端的返回数据 */
recv(ClientFd, Buf, BUF_LEN, 0);
printf(“服务端发送过来的数据为:%s\n”, Buf);

memset(Buf, 0, BUF_LEN); // 重置缓冲区
close(ClientFd); // 关闭套接字
}

return 0;
}

Linux下编译就不需要添加-lwsock32参数:

gcc linux_tcp_server.c -o linux_tcp_server
gcc linux_tcp_client.c -o linux_tcp_client

实验现象:

经上网查询发现是端口重复使用,可以在调用bind()函数之前调用setsockopt()函数以解决端口重复使用的问题: