TCPIP协议栈
TCPIP协议栈有四层:
- 链路层:是物理链接领域标准化的结果,专门定义WAN、LAN、MAN等网络标准,主机通过网络进行数据交换就需要这些物理连接
- IP层:用于决定每次传输时的路径选择,IP本身是面向消息的不可靠的协议,不处理传输数据错误丢失
- TCP/UDP层:完成实际数据传输,TCP协议可以保障可靠的数据
- 应用层:根据程序特点决定客户端和服务端之间的传输规定
实现基于TCP的服务端和客户端
服务端
TCP服务器默认函数调用顺序:
调用socket函数创建套接字,声明初始化地址信息结构体,调用bind函数分配地址
接下来调用listen函数:
int listen (int __fd, int __n);
参数1是进入接收请求状态的socket,参数2是请求连接队列长度
同时收到多个连接请求就会先放在队列里排队进行处理,请求队列长度根据实验结果而定
然后是受理客户端连接请求,调用accept函数:
int accept (int __fd, // 服务器socket
__SOCKADDR_ARG __addr, // 保存客户端地址
socklen_t *__restrict __addr_len // 第二个参数的长度
);
// 成功时返回套接字文件描述符,失败返回-1
accept函数处理请求队列里的请求,会自动创建一个新的套接字进行与客户端的通信
客户端
客户端函数调用顺序:
创建客户端套接字后向服务端请求连接,服务端调用listen函数之后创建连接等待队列,然后客户端即可请求连接,connect函数:
int connect(int sockfd, // 客户端socket
const struct sockaddr *addr,// 服务端地址信息
socklen_t addrlen // 第二个参数的长度
);
// 成功返回0,失败返回-1
connect函数在如下情况才会发生返回:
- 服务端接收连接请求,指的是服务端把请求记录到等待队列
- 发生断网等异常情况而中断连接请求
客户端的IP地址和端口号在调用connect函数时候自动分配,无需手动bind
客户端和服务端的调用关系
服务端创建套接字后调用bind,listen函数进入等待状态,客户端通过connect发起连接,服务端先于客户端的connect调用accept则会进入堵塞状态等待
实现迭代echo服务端客户端
之前实现的服务端在处理一次请求之后就会退出,需要继续受理后续客户端请求,需要对代码进行扩展
插入循环语句反复调用accept函数即可
服务端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
int main(int argc,char* argv[]){
int serv_sock,clnt_sock;
char message[BUF_SIZE];
int len_str,i;
sockaddr_in serv_addr,clnt_addr;
socklen_t clnt_addr_len;
if(argc != 2){
printf("usage:%s <port>\n", argv[0]);
return -1;
}
// 1. 创建套接字
serv_sock = socket(PF_INET,SOCK_STREAM,0);
// 2. 填充服务端地址信息,绑定地址到套接字
memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
bind(serv_sock,(const sockaddr*)&serv_addr,sizeof(serv_addr));
// 3. 设置套接字进入可监听状态
if(!listen(serv_sock,5)){
printf("Listening...\n");
}
// 4. 循环接收客户端请求并处理
clnt_addr_len = sizeof(clnt_addr);
while(true){
// 接收请求
clnt_sock = accept(serv_sock,(sockaddr*)&clnt_addr,&clnt_addr_len);
if(clnt_sock == -1){
printf("accept error\n");
continue;
}
printf("recv a connect requist\n");
// 处理单个请求,如果发送的
while (true){
memset(message,0,sizeof(message)-1);
int read_size = read(clnt_sock,message,sizeof(message)-1);
//fputs(message,stdout);
if(!strcmp(message,"Q\n") || !strcmp(message,"q\n")){
write(clnt_sock,"interrupt the connection\n",26);
break;
}else{
write(clnt_sock,message,read_size);
}
}
printf("close the connection\n");
close(clnt_sock);
}
// 5. 断开连接
close(serv_sock);
return 0;
}
客户端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
int main(int argc,char* argv[]){
int serv_sock;
sockaddr_in serv_addr;
if(argc !=3){
printf("usage:%s <ip-address> <port>\n",argv[0]);
return -1;
}
// 1.创建socket
serv_sock = socket(PF_INET,SOCK_STREAM,0);
// 2. 绑定socket信息
memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
// 3. 连接服务端
int ret = connect(serv_sock,(const sockaddr*)&serv_addr,sizeof(serv_addr));
if(ret==-1){
printf("connect failed!\n");
return -1;
}
printf("connect success!\n");
// 4. 数据交换
int read_size = 0;
while(true){
char msg_write[BUF_SIZE]={0};
char msg_read[BUF_SIZE]={0};
// 输出提示信息 + 读取输入(注意,这里scanf不行)
fputs("Input message(Q to quit) :",stdout);
fgets(msg_write,sizeof(msg_write)-1,stdin);
// 将获取到的输入发送给服务器
write(serv_sock,msg_write,strlen(msg_write));
// 从服务器那里读取数据
read_size = read(serv_sock,msg_read,sizeof(msg_read)-1);
msg_read[read_size] = 0;
// 打印收到的信息
printf("msg from server : %s",msg_read);
// 如果发送了q,则断开连接
if(!strcmp("q\n",msg_write) || !strcmp("Q\n",msg_write)){
break;
}
}
// 5. 断开连接
close(serv_sock);
return 0;
}
运行效果
selph@selph:~/NetProgramStudy/ch4-linux$ ./echo_client 127.0.0.1 11114
connect success!
Input message(Q to quit) :asd
msg from server : asd
Input message(Q to quit) :asd
msg from server : asd
Input message(Q to quit) :ad
msg from server : ad
Input message(Q to quit) :q
msg from server : interrupt the connection
存在的问题
客户端代码这一段:
// 将获取到的输入发送给服务器
write(serv_sock,msg_write,strlen(msg_write));
// 从服务器那里读取数据
read_size = read(serv_sock,msg_read,sizeof(msg_read)-1);
msg_read[read_size] = 0;
// 打印收到的信息
printf("msg from server : %s",msg_read);
每次调用read和write都会以字符串为单位执行实际IO操作,TCP不存在数据边界,多次调用write传递的字符串可能一次性到达服务器,可能客户端会一次性收到多个字符串,服务端可能一次write发不完数据会多次发
客户端也有可能在服务端write完成之前先read部分内容,读取不完整的字符串
这些问题来自于TCP数据传输特性,如何解决见下一章
这里没出现问题时因为运行环境太理想了
基于Windows实现
把Linux下的示例转换成Windows平台的,只要修改一下4点:
- 通过WSAStartup,WSACleanup函数初始化并清除套接字相关库
- 把数据类型和变量名切换成Windows风格(socket修改变量类型为SOCKET,SOCKADDR_IN大写)
- 传输中用recv和send函数而非read和write
- 关闭套接字用closesocket函数
服务端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <WinSock2.h>
#pragma comment(lib,"ws2_32.lib")
#define BUF_SIZE 1024
int main(int argc, char* argv[]) {
WSADATA wsaData = { 0 };
if (WSAStartup(MAKEWORD(2, 2), &wsaData)) { // WSAStartup成功返回0
exit(1);
}
SOCKET hServSock, hClntSock;
char message[BUF_SIZE];
int len_str, i;
SOCKADDR_IN serv_addr, clnt_addr;
int clnt_addr_len;
if (argc != 2) {
printf("usage:%s <port>\n", argv[0]);
return -1;
}
// 1. 创建套接字
hServSock = socket(PF_INET, SOCK_STREAM, 0);
// 2. 填充服务端地址信息,绑定地址到套接字
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
bind(hServSock, (const sockaddr*)&serv_addr, sizeof(serv_addr));
// 3. 设置套接字进入可监听状态
if (!listen(hServSock, 5)) {
printf("Listening...\n");
}
// 4. 循环接收客户端请求并处理
clnt_addr_len = sizeof(clnt_addr);
while (true) {
// 接收请求
hClntSock = accept(hServSock, (sockaddr*)&clnt_addr, &clnt_addr_len);
if (hClntSock == -1) {
printf("accept error\n");
continue;
}
printf("recv a connect requist\n");
// 处理单个请求,如果发送的
while (true) {
memset(message, 0, sizeof(message) - 1);
int read_size = recv(hClntSock, message, sizeof(message) - 1,0);
//fputs(message,stdout);
if (!strcmp(message, "Q\n") || !strcmp(message, "q\n")) {
send(hClntSock, "interrupt the connection\n", 26,0);
break;
}
else {
send(hClntSock, message, read_size,0);
}
}
printf("close the connection\n");
closesocket(hClntSock);
}
// 5. 断开连接
closesocket(hServSock);
WSACleanup();
return 0;
}
客户端代码
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <WinSock2.h>
#pragma comment(lib,"ws2_32.lib")
#define BUF_SIZE 1024
int main(int argc, char* argv[]) {
SOCKET hSock;
SOCKADDR_IN serv_addr;
WSADATA wsaData = { 0 };
if (WSAStartup(MAKEWORD(2, 2), &wsaData)) { // WSAStartup成功返回0
exit(1);
}
if (argc != 3) {
printf("usage:%s <ip-address> <port>\n", argv[0]);
return -1;
}
// 1.创建socket
hSock = socket(PF_INET, SOCK_STREAM, 0);
// 2. 绑定socket信息
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
// 3. 连接服务端
int ret = connect(hSock, (const sockaddr*)&serv_addr, sizeof(serv_addr));
if (ret == -1) {
printf("connect failed!\n");
return -1;
}
printf("connect success!\n");
// 4. 数据交换
int read_size = 0;
while (true) {
char msg_write[BUF_SIZE] = { 0 };
char msg_read[BUF_SIZE] = { 0 };
// 输出提示信息 + 读取输入(注意,这里scanf不行)
fputs("Input message(Q to quit) :", stdout);
fgets(msg_write, sizeof(msg_write) - 1, stdin);
// 将获取到的输入发送给服务器
send(hSock, msg_write, strlen(msg_write),0);
// 从服务器那里读取数据
read_size = recv(hSock, msg_read, sizeof(msg_read) - 1,0);
msg_read[read_size] = 0;
// 打印收到的信息
printf("msg from server : %s", msg_read);
// 如果发送了q,则断开连接
if (!strcmp("q\n", msg_write) || !strcmp("Q\n", msg_write)) {
break;
}
}
// 5. 断开连接
closesocket(hSock);
WSACleanup();
return 0;
}
运行效果
PS C:\Users\selph\source\Book\TCPIP\ch4-windows\x64\Debug> .\ch4-windows-client.exe 127.0.0.1 12345
connect success!
Input message(Q to quit) :hello selph
msg from server : hello selph
Input message(Q to quit) :q
msg from server : interrupt the connection