最近学习linux socket编程,看看unp那本书,顺便写了个类似最简单聊天功能的软件,界面是用qt写的,写下来总结总结吧,如果有问题,欢迎大家和我交流。
模式是C/S模式,服务器端等待请求,客户端发送后建立请求。连接用的是tcp不是udp,其实udp实现更为简单。
一. 环境搭建
我用docker搭了几个虚拟机,具体搭建方式可以参考网上的或我之前写docker的总结:点击打开链接
docker还是很轻量级的,比一般虚拟机起的快多了。
搭建好docker以后就是基本的先看网络是否联通,如果不连通看看链路,防火墙设置一下。
二.没有界面的代码实现:
这部分代码主要参考unp上的简单例子,其中肯定会有bug,因为没有考虑复杂的tcp环境,只是单纯的先实现再说:
服务器端:
#include "unp.h"
#include
#include
void Answer(FILE *fop, FILE *fip, int sockfd) {
int maxfdp1, stdineof;
fd_set rset;
char buf[MAXLINE];
int n;
stdineof = 0;
FD_ZERO(&rset);
for ( ; ; ) {
if (stdineof == 0)
FD_SET(fileno(fip), &rset);
FD_SET(sockfd, &rset);
maxfdp1 = max(fileno(fip), sockfd) + 1;
Select(maxfdp1, &rset, NULL, NULL, NULL);
if (FD_ISSET(sockfd, &rset)) { /* socket is readable */
if ( (n = Read(sockfd, buf, MAXLINE)) == 0) {
if (stdineof == 1)
return; /* normal termination */
else
err_quit("client quit!");
}
Write(fileno(fop), buf, n);
}
if (FD_ISSET(fileno(fip), &rset)) { /* input is readable */
if ( (n = Read(fileno(fip), buf, MAXLINE)) == 0) {
stdineof = 1;
Shutdown(sockfd, SHUT_WR); /* send FIN */
FD_CLR(fileno(fip), &rset);
continue;
}
Writen(sockfd, buf, n);
}
}
}
int main(int argc, char **argv)
{
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
//servaddr.sin_port = htons(SERV_PORT);
servaddr.sin_port = htons(50001);
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
for ( ; ; ) {
clilen = sizeof(cliaddr);
if ( (connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < 0) {
if (errno == EINTR)
continue; /* back to for() */
else
err_sys("accept error");
}
if ( (childpid = Fork()) == 0) { /* child process */
Close(listenfd); /* close listening socket */
Answer(stdout, stdin, connfd);
printf("end of one connect!\n");
exit(0);
}
Close(connfd); /* parent closes connected socket */
}
}
客户端代码:
#include "unp.h"
const int SIZE = 1024;
void Input(FILE *fp, int sockfd) {
int maxfdp1, stdineof;
fd_set rset;
char buf[MAXLINE];
int n;
stdineof = 0;
FD_ZERO(&rset);
for ( ; ; ) {
if (stdineof == 0)
FD_SET(fileno(fp), &rset);
FD_SET(sockfd, &rset);
maxfdp1 = max(fileno(fp), sockfd) + 1;
Select(maxfdp1, &rset, NULL, NULL, NULL);
if (FD_ISSET(sockfd, &rset)) { /* socket is readable */
if ( (n = Read(sockfd, buf, MAXLINE)) == 0) {
if (stdineof == 1)
return; /* normal termination */
else
err_quit("str_cli: server terminated prematurely");
}
Write(fileno(stdout), buf, n);
}
if (FD_ISSET(fileno(fp), &rset)) { /* input is readable */
if ( (n = Read(fileno(fp), buf, MAXLINE)) == 0) {
stdineof = 1;
Shutdown(sockfd, SHUT_WR); /* send FIN */
FD_CLR(fileno(fp), &rset);
continue;
}
Writen(sockfd, buf, n);
}
}
}
int main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in servaddr;
if (argc != 3)
err_quit("usage: tcpcli ::");
sockfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(atoi(argv[2]));
Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
Connect(sockfd, (SA *) &servaddr, sizeof(servaddr));
struct sockaddr_in ss;
socklen_t len = sizeof(ss);
if(getsockname(sockfd, (SA *)&ss, &len) < 0) return 0;
int clientPort = ntohs(ss.sin_port); //pay attention the translate n<->pstr_cli(stdin, sockfd); /* do it all */
printf("%d\n", clientPort);
Input(stdin, sockfd);
exit(0);
}
要想跑通这个代码还得配置unp的环境。好吧,我下面有界面的代码可以不用配置unp环境
三.加界面
我抽象了几个基类,主要界面就4个,先来看看界面吧:
服务器端和客户端的聊天界面是一样的,服务器端我做了点小处理,拒绝多个客户的连接,当一个客户聊天时,监听的socket就被关闭了。
刚学qt就开始搞界面,真是太恶心了。
主要代码:
客户端登录:
#include "clientstart.h"
#include "chatclient.h"
#include
#include
#include
using namespace std;
ClientStart::ClientStart(QWidget *parent) :
StartBase(parent)
{
this->className = "ClientStart";
/*belong to first layout*/
QLabel *labelOppositeIP = new QLabel("请输入服务器IP:");
oppositeIP = new QLineEdit;
QLabel *labelOppositePort = new QLabel("请输入服务器端口:");
oppositePort = new QLineEdit;
firstLayout->addWidget(labelOppositeIP, 2, 0, 1, 1);
firstLayout->addWidget(oppositeIP, 2, 1, 1, 4);
firstLayout->addWidget(labelOppositePort, 3, 0, 1, 1);
firstLayout->addWidget(oppositePort, 3, 1, 1, 4);
/*second layout*/
secondLayout = new QHBoxLayout;
secondLayout->setSpacing(60);
secondLayout->addWidget(buttonSure);
secondLayout->addWidget(buttonCancel);
topLayout->addLayout(firstLayout);
topLayout->addLayout(secondLayout);
QWidget* widget = new QWidget(this);
widget->setLayout(topLayout);
this->setCentralWidget(widget); //add the topLayout in current dialog
}
ClientStart::~ClientStart()
{
//delete this;
}
void ClientStart::doOpen() {
//QMessageBox::information(this, "success", QObject::tr("xxx"), QMessageBox::Ok);
serverIP = (oppositeIP->text()).toStdString();
serverPort = (oppositePort->text()).toStdString();
/*start connect now*/
bool isConnect = tryConnect();
if(!isConnect) {
errorCall(this, "连接失败");
return ;
}
ChatClient cc(clientIP, clientPort, serverIP, serverPort, servaddr, clientSocketFd);
cc.exec();
//cc.show();
this->close();
}
void ClientStart::doCancel() {
QMessageBox::information(this, "failed", QObject::tr("请重新设置"), QMessageBox::Ok);
oppositeIP->setText("");
oppositePort->setText("");
}
bool ClientStart::tryConnect() {
clientSocketFd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(atoi(serverPort.c_str()));
inet_pton(AF_INET, serverIP.c_str(), &servaddr.sin_addr);
if(::connect(clientSocketFd, (SA *) &servaddr, sizeof(servaddr)) < 0) return 0;
/*get client port, ip has already got*/
struct sockaddr_in ss;
socklen_t len = sizeof(ss);
if(getsockname(clientSocketFd, (SA *)&ss, &len) < 0) return 0;
stringstream stmp; //change int to string
stmp << ntohs(ss.sin_port); //pay attention the translate n<->p
clientPort = stmp.str();
return 1;
}
#include "chatclient.h"
#include
#include
using namespace std;
ChatClient::ChatClient(std::string clientIP, std::string clientPort, std::string serverIP, std::string serverPort, struct sockaddr_in servaddr, int clientSocketFd)
: ChatBase(NULL) {
this->clientIP = clientIP;
this->clientPort = clientPort;
this->serverIP = serverIP;
this->serverPort = serverPort;
this->servaddr = servaddr;
this->clientSocketFd = clientSocketFd;
this->lineLocalIP->setText(QString(clientIP.c_str()));
this->lineLocalPort->setText(QString(clientPort.c_str()));
this->lineOppositeIP->setText(QString(serverIP.c_str()));
this->lineOppositePort->setText(QString(serverPort.c_str()));
this->secondLayout->setReadOnly(1);
this->secondLayout->setText("start chatting now!\n");
this->secondLayout->append(showTime());
this->secondLayout->show();
/*connect the socket and function*/
stdineof = 0;
FD_ZERO(&rset);
revSN = new QSocketNotifier(clientSocketFd, QSocketNotifier::Read);
QObject::connect(revSN, SIGNAL(activated(int)), this, SLOT(doDataReceived()));
}
ChatClient::~ChatClient() {
stdineof = 1;
shutdown(clientSocketFd, SHUT_WR); /* send FIN */
FD_ZERO(&rset);
this->close();
}
void ChatClient::doSendMsg() {
std::string sent = (forthLayout->toPlainText()).toStdString();
sent += '\n';
if(write(clientSocketFd, sent.c_str(), sent.size()) < 0) {
errorCall(this, "send message failed!");
return ;
}
appendSecondLayout(sent);
forthLayout->clear();
}
void ChatClient::doCancel() {
forthLayout->clear();
}
void ChatClient::doSendFile() {}
void ChatClient::doVedio() {}
void ChatClient::doDataReceived() {
int revLen;
if ( (revLen = read(clientSocketFd, buf, MAXLINE)) == 0) {
if (stdineof == 1)
return; /* normal termination */
else {
errorCall(this, "server terminated prematurely");
this->close();
}
}
buf[revLen] = '\0';
appendSecondLayout(string(buf));
return ;
}
我把全部代码上传到csdn了,链接:点击打开链接
安装时需要配置qt5,视频和传文件功能暂未实现
四.总结和未来展望
这个软件也就写着玩,肯定有Bug。其实之前的计划是作NAT转发,使得两台局域网之间的主机可以直接聊天,后来发现行不通,没有权限修改入网的路由NAT规则。
以后的计划如下:
1.是不用qt界面,直接命令行。
2.实现文件传输。
3.抛弃C/S架构,搞一台有公网ip的服务器,两台聊天的主机同时连到这个服务,服务器进行转发
如果大家有什么问题欢迎交流,希望能有小伙伴和我一起来搞~~。
代码虽不难,但也是自己倒腾的,转载请注明地址:http://blog.csdn.net/u011353822/article/details/41246277