简单的PHP聊天小技巧

3
我一直在开发一个快速简单的jQuery/PHP聊天系统,以供访问者进行交流。我估计网站同时在线用户数量最高可达200人,实际聊天人数最多不超过10-20人。
以下是一些问题:
我已经遇到过两次这样的情况(尽管它似乎更像是一个不太可能发生的事件,而不是在执行特定操作后发生的事情),即聊天系统会加载已读消息并将其显示出来。
为了保持聊天系统的简洁性,我编写了以下代码:
HTML代码:
<div class="chat">

    <ul class="chat">

        <li class="chat" >

            <h5 class="chat">Date</h5>
            <h6 class="chat">Time</h6>
            <h4 class="chat">User</h4>
            <br/>
            <q class="chat">Message</q>

        </li>

    </ul>

    <input class="chat" placeholder="write something..."/>

</div>

您可以看到,我放置了一个占位符li元素,供jQuery使用作为片段来创建具有实际消息的新li元素,并将它们prependul元素内部。


jQuery代码:

发送消息:

$(document).ready(function(){

    chatSnippet = $('ul.chat').html(); // here chatSnippet is a global variable
    $('ul.chat').html('');

    $('input.chat').change(function(event){// Send your message

    message = $(this).attr('value');

// first thing I perform an asynchronous POST to the receiving php script

    $.post(

        'php/chatRec.php',

        {

            user : currentUser,
            message: message,

        }

    );

// meanwhile I add a new li element to the chat html with the content just submitted


    date.setTime(event.timeStamp);

    hours = ''+date.getHours();

    if(hours.length < 2) hours = '0'+hours;

    minutes = ''+date.getMinutes();

    if(minutes.length < 2) minutes = '0'+minutes;

    day = ''+date.getDate();

    if(day.length < 2) day = '0'+day;

    newChatMessage = chatSnippet.replace('Date', ''+day+' '+months[date.getMonth()]);
    // here months is an array with the months names (in italian)
    newChatMessage = newChatMessage.replace('Time', ''+hours+':'+minutes);

    newChatMessage = newChatMessage.replace('User', connectedUser);

    newChatMessage = newChatMessage.replace('Message', message);

    $mess = $(newChatMessage);

    $mess.hide().prependTo('ul.chat').fadeIn(500);

    $(this).attr('value','');

});

refreshChat(''); // this function retrives new messages from the DB

// Here I perform a void refreshChat call so I'll get all the messages in the DB regardless from the currentUser (at page refresh)

});

接收消息:

// This code is placed outside (before) the .ready function

function refreshChat(user){// Receiving messages

$.post(

    'php/chatInv.php',

    {

        user : user,
        token: lastMessage // this variable contains the token of the last red message

    },

    function(data){

        receivedMessages = jQuery.parseJSON(data);

        for(message in receivedMessages){

            message = receivedMessages[message].Message;

            date = receivedMessages[message].Day.split('-');
            time = receivedMessages[message].Time.split(':');

            newChatMessage = chatSnippet.replace('Date', ''+date[2]+' '+months[parseInt(date[1])-1]);

            newChatMessage = newChatMessage.replace('Time', ''+time[0]+':'+time[1]);

            newChatMessage = newChatMessage.replace('User', receivedMessages[message].Sender);

            newChatMessage = newChatMessage.replace('Message', message);

            $mess = $(newChatMessage);

            $mess.hide().prependTo('ul.chat').fadeIn(500);

            lastMessage = receivedMessages[messages].token;

        }

        nextRefresh = setTimeout("refreshChat('"+currentUser+"')",2000);

// When I'm done I set a timeout of 2 secs and then perform another refresh

    }

);

}

PHP代码:

接收新消息(我认为问题就在这里):

    mysql_connect("localhost", "root", "root") or die(mysql_error());
    mysql_select_db("chat") or die(mysql_error());

    $characters = array('0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z');

    $token = $characters[rand(0,61)].$characters[rand(0,61)].$characters[rand(0,61)].$characters[rand(0,61)].$characters[rand(0,61)];

    $all_Msgs = mysql_query("SELECT * FROM Messages ORDER BY ID");

    $prev_Msg = array('ID' => 1 , 'Sender' => $_POST['user'], 'Message' => $_POST['message'], 'Day' => date("Y-m-d"), 'Time' => date("H:i:s"), 'token' => $token);

    while($Msg = mysql_fetch_array($all_Msgs)){

        $update_success = mysql_query("UPDATE Messages SET Sender='".$prev_Msg['Sender']."', Message='".$prev_Msg['Message']."', Day='".$prev_Msg['Day']."', Time='".$prev_Msg['Time']."', token = '".$prev_Msg['token']."' WHERE ID=".$Msg['ID']);

        $prev_Msg = $Msg;

    }

我在这里做的基本上是接收新帖子消息,生成一个令牌和一个包含新输入数据的数组元素(它本身就是一个数组),然后在一个固定大小的SQL表上执行一系列UPDATE语句,覆盖第一条记录上的新数据,然后覆盖每个记录与前一个记录(以便最后一条记录最终会丢失)。

发送消息:

    mysql_connect("localhost", "root", "root") or die(mysql_error());
    mysql_select_db("chat") or die(mysql_error());

    $receiver = $_POST['user'];
    $token = $_POST['token'];

    $all_Msgs = mysql_query("SELECT * FROM Messages ORDER BY ID");

    $newMessages = array();

    while($Msg = mysql_fetch_array($all_Msgs)){

        if($Msg['token'] == $token) break;

        if($Msg['Sender'] != $receiver) array_unshift($newMessages,$Msg);

    }

    echo json_encode($newMessages);

所以我向客户端发送了一个JSON编码的数组,其中包含最后已知消息之后插入的数据库中所有记录,并且作者不是查询客户端。

我的怀疑:

我得出结论,在执行消息接收(服务器端)时,每个消息都从数据库中取出一段时间,如果同时进行刷新,则找不到该消息,如果该消息是我们正在寻找的作为最后一条红色消息,则服务器将只选择表中的所有消息并将它们发送回来。

结果是您会看到一堆您已经阅读过的消息,而您的消息不在其中(因为它们是在视图客户端中添加的,并且服务器脚本不会将您的消息发送回来)

声明如下:

  • 我不在意消息是否按实际插入顺序排列:假设A和B正在聊天,实际的真实消息顺序是BAB,但A可能会看到ABB的顺序,因为他的视图在输入时立即更新(这有助于保持“快速实时”感觉)
  • 我不在意某些消息是否丢失(例如,如果它在某人阅读之前跌落到固定的DB表边缘)
  • 此时,我不太关心实际效率,速度和优化
  • 我知道我应该以不同的方式处理消息插入,先添加新记录,然后仅更新ID并删除最后一条记录。但如果可能,我想保持这种仅更新的方式。

您认为我对问题的解释正确吗? 如果不是:那么原因是什么?/我该如何修复? 如果是:我该如何轻松解决?

如果实际修复相当复杂:在10-20个用户聊天中出现这种怪癖的可能性有多大?

谢谢


1
友情提醒:注意防范SQL注入攻击! :-) 希望在这个初始原型之后,您已经计划添加此类保护措施,但我个人建议从一开始就实施。 - Wiseguy
好的,我会的。其实我这里有一些格式化/转义函数,只是在这里删掉了代码 :D - Carlo Moretti
4
好的,收到。更好的是,通过使用PDO或MySQLi来采用参数化查询的方式,可以避免完全混乱的转义问题。 - Wiseguy
1
@Onheiron mysql_query在PHP 7中被淘汰了,所以现在是时候转变了。我强烈建议至少使用PDO,或者像DoctrinePropel这样的ORM,如果你想要做得正确。更好的选择是使用开发框架,比如Laravel,这将使第一次正确地完成变得更加容易。 - tadman
1个回答

1
我在处理聊天代码时也注意到了这一点,解决方法是将最后一条消息的ID(在MySQL中设置为自动递增字段)存储在会话中,并搜索数据库以获取ID大于该值的消息,而不是使用time()函数。
if (!$_SESSION['message_id']]) {
// if there isn't a message_id, select the last seven entries in the message list
    $sql = "SELECT messages.message_id, messages.message, users.username FROM (SELECT * FROM messages, users user.user_id = messages.user_id ORDER BY message_id DESC LIMIT 7) as new_tbl ORDER BY message_id ASC";
} else {
// if there is a message_id, select the messages sent since the last entry
    $sql = sprintf("SELECT messages.message_id, messages.message, users.username FROM messages, users WHERE user.user_id = messages.user_id message_id > '%d'", $_SESSION['message_id']);
}

$data = array();
$query = mysql_query($sql);
while ($row = mysql_fetch_array($query)) {
// build the data array from the mysql result and set the message_id session to the id of the last message
    $data[$i] = array('user' => $row['username'], 'message' => $row['message']);
    $_SESSION['message_id'] = $row['message_id'] > $_SESSION['message_id'] ? $row['message_id'] : $_SESSION['message_id'];
    $i++;
}

显然,您需要转义会话!

如果没有message_id会话,则从表中加载最后7条消息(按降序排序,然后按升序排序这些消息)。如果有message_id会话,则加载新消息。

在while循环中,它构建一个数据数组(我将其作为JSON发送到我的脚本),并将message_id会话设置为message_id行,并进行故障安全检查,以确保message_id会话不会被降低。

SQL意味着您拥有一个用户表,其中包含user_id和username,以及一个消息表,其中包含user_id、message_id和message。


这实际上非常好:如果我将搜索绑定到“大于”而不是“等于”,即使最后一个红色消息不在表中,我也能够停止! - Carlo Moretti
我基本上遇到了你现在面临的所有问题,这是我想出的唯一解决方案。我每0.2秒刷新一次聊天记录,并且不会出现重复。 - Andrew Willis
哇,这听起来不错!0.2秒的频率不算太高吗?我担心会遇到流量/拥堵问题,所以将其设置为2秒...是否有类似于每秒最大请求或服务器可以处理的某些东西?谢谢。 - Carlo Moretti
1
这取决于服务器,但通常是按数据的百分比和同时最大进程数计算(如果您正确地索引表并限制查询,则这些进程不会花费太长时间!),如果您发送请求并返回无数据,则带宽使用率将很小。当我说每0.2秒刷新一次时,那是超时时间,但在进行检查后,实际上是每0.4秒刷新一次,因为当请求完成时超时就会触发,所以请求也需要0.2秒的时间。 - Andrew Willis
如果你喜欢翻阅四年前的答案并发表讽刺性评论,那就去吧!我只是提供了一个例子,并没有想到用户会直接访问会话全局变量,加入多余的代码来证明一点只会让答案更加复杂。我的解决方案是将ID存储在会话中,这比展示如何转义数据更加相关,我认为任何人都应该知道如何转义数据。 - Andrew Willis
我没有意识到这个问题的年龄,它因某种原因被推到了第一个MySQL页面。我已经撤回了我的评论。 - tadman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接