TCP编程
流程
客户端: 发送请求
服务器端: 相应请求
服务器端(server):
1) socket(),创建套接字文件,创建出用于连接的套接字文件
2) bind(), 绑定,把socket()函数返回的文件描述符和IP、端口号进行绑定;
3) listen(), 监听,将socket()返回的文件描述符,由主动套接字变为被动套接字;
4) accept(), 阻塞函数,阻塞等待客户端的连接请求, 返回一个用于通信的套接字文件;
5) recv(), 接收客户端发来的数据;(read)
//6) send(), 发送数据;(write)
7) close(), 关闭文件描述符; 至少要关闭: 连接、通信
客户端(client):
1. socket(),创建套接字文件,既用于连接,也用于通信;
填充结构体: 填充服务器的ip和端口 , 用于connect连接
2) connect(); 用于发起连接请求,阻塞等待连接服务器;
3) send(), 发送数据;
//4) recv(), 接收数据;
5)close(), 关闭文件描述符;
函数接口:
服务器 server:
1. socket创建套接字
int socket(int domain, int type, int protocol);
头文件: #include <sys/types.h> #include <sys/socket.h>
功能:创建套接字
参数:
domain:协议族
AF_UNIX, AF_LOCAL 本地通信
AF_INET ipv4
AF_INET6 ipv6
type:套接字类型
SOCK_STREAM:流式套接字
SOCK_DGRAM:数据报套接字
protocol:协议 - 填0 自动匹配底层 ,根据type
系统默认自动帮助匹配对应协议
传输层:IPPROTO_TCP、IPPROTO_UDP、IPPROTO_ICMP
网络层:htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL)
返回值:
成功 文件描述符 0 -> 标准输入 1->标准输出 2->标准出错
3->socket
失败 -1,更新errno
//1.创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("socket err");
}
printf("socket: %d\n", sockfd);
2. bind绑定套接字
头文件: #include<sys/types.h> #include<sys/socket.h>
#include<netinet/in.h> #include<netinet/ip.h>
功能:绑定协议, IP以及port
声明: int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
sockfd:套接字
addr:用于通信结构体 (提供的是通用结构体,需要根据选择通信方式,填充对应 结构体-通信当时socket第一个参数确定,需要强转)
addrlen:结构体大小
返回值:成功 0 失败-1,更新errno
通用结构体:
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
ipv4通信结构体:
struct sockaddr_in {
sa_family_t sin_family; ----协议族
in_port_t sin_port; ----端口
struct in_addr sin_addr; ----ip结构体
};
struct in_addr {
uint32_t s_addr; --ip地址
};
//填充结构体 服务器
struct sockaddr_in saddr, caddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(8889);
saddr.sin_addr.s_addr = inet_addr("192.168.51.192");
int len = sizeof(caddr);
//bind 绑定ip
if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
{
perror("bind err");
return -1;
}
3. listen监听
int listen(int sockfd, int backlog);
功能:监听,将主动套接字变为被动套接字
参数:
sockfd:套接字
backlog:同一时间可以响应客户端请求链接的最大个数,不能写0.
不同平台可同时链接的数不同,一般写6-8个
返回值:成功 0 失败-1,更新errno
if (listen(sockfd, 5) < 0)
{
perror("listen err");
return -1;
}
printf("listen success\n");
4. accept阻等待连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept(sockfd,NULL,NULL);
功能:阻塞函数,阻塞等待客户端的连接请求,如果有客户端连接,
则accept()函数返回,返回一个用于通信的套接字文件;
参数:
Sockfd :套接字
addr: 链接客户端的ip和端口号
如果不需要关心具体是哪一个客户端,那么可以填NULL;
addrlen:结构体的大小
如果不需要关心具体是哪一个客户端,那么可以填NULL;
返回值:
成功:文件描述符; //用于通信
失败:-1,更新errno新errno
int acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);
if (acceptfd < 0)
{
perror("accept err");
return -1;
}
printf("accept succes\n");
在成功接受连接后,accept() 函数会将客户端的地址信息填充到 caddr 结构体中,以供后续使用
5. recv
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
功能: 接收数据
参数:
sockfd: acceptfd ;
buf 存放位置
len 大小
flags 一般填0,相当于read()函数
MSG_DONTWAIT 非阻塞
返回值:
< 0 失败出错 更新errno
==0 表示客户端退出
>0 成功接收的字节个数
int recvbyte = recv(acceptfd, buf, sizeof(buf), 0);
if (recvbyte < 0)
{
perror("recv err");
return -1;
}
else if (recvbyte == 0)
{
printf("ip: %s port : %d client is exit\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
break;
}
else
{
printf("%s\n", buf);
}
总结
服务器:
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
//1.创建套接字 >> 返回一个建立连接的文件描述符
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd < 0)
{
perror("socket is err:");
}
printf("sockfd : %d\n",sockfd); //3
//2. 填充结构体
struct sockaddr_in saddr,caddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(argv[1]);
saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
int len = sizeof(caddr);
//bind绑定ip和端口
if(bind(sockfd,(struct sockaddr *)&saddr,sizeof(saddr))<0)
{
perror("bind is err:");
return -1;
}
//3. listen 监听
if(listen(sockfd,5)<0)
{
perror("listen is err:");
return -1;
}
printf("listen is success\n");
//4. 阻塞 等待客户端连接 -> 返回一个同于通信的文件描述符
int acceptfd = accept(sockfd,(struct sockaddr *)&caddr,&len);
if(acceptfd < 0)
{
perror("accept is err:");
return -1;
}
char buf[128] = "";
while(1)
{ //recv接收客户端发送的内容
int recvbyte = recv(acceptfd,buf,sizeof(buf),0);
if(recvbyte < 0) //函数出错
{
perror("recv is err:");
return -1;
}
else if(recvbyte == 0) //表示客户端退出
{
printf("ip: %s port : %d client is exit\n",\
inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port));
break;
}
else //打印客户端的内容
{
printf("%s\n",buf);
}
}
close(acceptfd);
close(sockfd);
return 0;
}
客户端:
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
//1.创建socket套接字 - 仅仅具有连接的功能
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd < 0)
{
perror("socket is err:");
return -1;
}
//2.填充结构体
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[1]));
saddr.sin_addr.s_addr = inet_addr(argv[2]);
//连接成功之后, sockfd同时具有通信的功能
if(connect(sockfd,(struct sockaddr *)&saddr,sizeof(saddr)) < 0)
{
perror("connect is err:");
return -1;
}
char buf[128] = "";
while(1)
{
//终端输入, 内容存于 buf 内
fgets(buf,sizeof(buf),stdin);
if(buf[strlen(buf)-1] == '\n')
buf[strlen(buf)-1] = '\0';
//发送buf的内容
send(sockfd,buf,sizeof(buf),0);
}
//关闭文件描述符
close(sockfd);
return 0;
}
优化代码
1.客户端发送去掉fgets获取的多余的'\n'.
fgets(实际读到的内容小于等于指定个数-1,自动读到的内容后添加’\0’,会将’\n’也读入)
if(buf[strlen(buf)-1] == '\n')//去掉fgets获取的'\n'
buf[strlen(buf)-1] ='\0';
2.客户端端口和ip地址通过命令行传参到代码中;(如果参数不一致,应该提示并退出)
3.设置客户端退出,服务器结束循环接收。
通过recv返回值为0判断客户端是否退出
4.设置来电显示功能,获取到请求链接服务器的客户端的ip和端口。
int acceptfd = accept(sockfd,(struct sockaddr *)&caddr,&len);
打印时记得使用 inet_ntoa()和ntohs()转换为主机字节序
5.设置服务器端自动获取自己的ip地址。
inet_addr("0.0.0.0")
6.实现循环服务器,服务器不退出,当链接服务器的客户端退出,服务器等到下一个客户端链接。
accept处使用while
7. 当客户端输入quit的时候,客户端和服务器服务器都要退出
strncmp
优化版本
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>
int main()
{
// 创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("socket error");
return -1;
}
// 设置套接字选项以允许地址重用
int reuse = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(int)) == -1)
{
perror("setsockopt error");
close(sockfd);
return -1;
}
// 配置服务器地址结构体
struct sockaddr_in saddr, caddr;
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(8889);
saddr.sin_addr.s_addr = INADDR_ANY;
// 绑定套接字到指定地址
if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) == -1)
{
perror("bind error");
close(sockfd);
return -1;
}
// 监听连接请求
if (listen(sockfd, 5) == -1)
{
perror("listen error");
close(sockfd);
return -1;
}
printf("Server is listening for connections...\n");
while (1)
{
socklen_t len = sizeof(caddr);
// 接受客户端连接请求
int acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);
if (acceptfd == -1)
{
perror("accept error");
close(sockfd);
return -1;
}
// 打印客户端连接信息
printf("Accepted connection from %s:%d\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
char buf[128];
while (1)
{
// 接收客户端发送的数据
ssize_t recvbyte = recv(acceptfd, buf, sizeof(buf) - 1, 0);
if (recvbyte == -1)
{
perror("recv error");
close(acceptfd);
break;
}
else if (recvbyte == 0)
{
printf("Connection closed by client\n");
close(acceptfd);
break;
}
else
{
buf[recvbyte] = '\0';
printf("Received: %s", buf);
}
}
close(acceptfd); // Close the client socket after processing
}
close(sockfd);
return 0;
}
UDP编程 介绍
通信流程 --- 无连接(connect accept)的过程
UDP 无法判断客户端是否退出:
使用心跳包: 使用客户端, 定时给服务器发送内容
udp流程:(类似发短信)
server:
创建数据报套接字(socket(,SOCK_DGRAM,))----->有手机
绑定网络信息(bind())-----------> 绑定IP和port(发短信知道发给谁)
接收信息(recvfrom())------------>接收信息,同时可以获取到发送者的IP和port
关闭套接字(close())-------------->接收完毕
client:
创建数据报套接字(socket())----------------------->有手机
指定服务器的网络信息------------------------------>有对方号码
发送信息(sendto())---------------------------->发送短信,根据填充的结构体信息
关闭套接字(close())--------------------------->发送完
函数接口
recvfrom
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
功能:接收数据
参数:
sockfd:套接字描述符
buf:接收缓存区的首地址
len:接收缓存区的大小
flags:0 接收数据 并 阻塞
MSG_DONTWAIT: 设置非阻塞
src_addr: 发送端的网络信息结构体的指针(对方的 caddr)
addrlen:发送端的网络信息结构体的大小的指针(对方的 caddr)
返回值:
成功接收的字节个数
接收到的数据为0 : 0
失败:-1
int recvbyte = recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&caddr, &len);
if (recvbyte < 0)
{
perror("recvfrom is err ");
return -1;
}
sendto
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
功能:发送数据
参数:
sockfd:套接字描述符
buf:发送缓存区的首地址
len:发送缓存区的大小
flags:0 发送消息并阻塞
src_addr:接收端的网络信息结构体的指针
addrlen:接收端的网络信息结构体的大小
返回值:
成功发送的字节个数
失败:-1
sendto(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&saddr, sizeof(saddr));
注意:
1、对于TCP是先运行服务器,客户端才能运行。
2、 对于UDP来说,服务器和客户端运行顺序没有先后,因为是无连接,所以服务器和客户端谁先开始,没有关系,
3、 UDP一个服务器可以同时连接多个客户端。想知道是哪个客户端登录,可以在服务器代码里面打印IP和端口号。
以下内容面试可能会问: 感兴趣可以自己测试一下
4、UDP,客户端当使用send的时候,上面需要加connect,,这个connect不是代表连接的作用,而是指定客户端即将要发送给谁数据。这样就不需要使用sendto而用send就可以。
5、在TCP里面,也可以使用recvfrom和sendto,使用的时候将后面的两个参数都写为NULL就OK。
send,sendto 区别
send(),recv()用于TCP,sendto()及recvfrom()用于UDP
但是send(),recv()也可以用于UDP,sendto()及recvfrom()也可以用于TCP
sendto可以在参数中指定发送的目标地址 , send需要socket已建立连接, sendto 可用于无连接的 socket 对于send的有连接socket,两者一样,sendto最后两个参数没用.
总结
服务器:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
perror("socket is err");
return -1;
}
struct sockaddr_in saddr, caddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[1]));
saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
int len = sizeof(caddr);
if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
{
perror("bind is err ");
return -1;
}
char buf[128] = "";
while (1)
{
int recvbyte = recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&caddr, &len);
if (recvbyte < 0)
{
perror("recvfrom is err ");
return -1;
}
else
{
printf("ip: %s port:%d %s",saddr.sin_addr,saddr.sin_port,buf);
}
}
close(sockfd);
return 0;
}
客户端:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
perror("socket is err ");
return -1;
}
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[2]));
saddr.sin_addr.s_addr = inet_addr(argv[1]);
char buf[128];
while (1)
{
fgets(buf, sizeof(buf), stdin);
if (buf[strlen(buf) - 1] == '\n')
buf[strlen(buf) - 1] = '0';
sendto(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&saddr, sizeof(saddr));
}
close(sockfd);
return 0;
}